xm_ip_service

xm_ip_service

相关技术栈:golang、MySQL、PostgreSQL、linux、Web

项目需求

通过IP地址(目前仅支持IPV4),进行IP地址相关地理信息的查询,查询过程分为,国内信息查询与国际信息查询两个查询方式。

接口定义

接口内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
struct GeoInfo {
1: string ip,
2: string geoip_country_code,
3: string geoip_country_name,
4: string geoip_city_code,
5: string geoip_city_name,
6: i32 error_no,
7: string geoip_province_code,
8: string geoip_province_name,
9: string geoip_carrier,
10: string error_msg,
11: i64 geoip_start_ip,
12: i64 geoip_end_ip
}

struct GeoRequest {
1: list<string> ipList,
2: string lang // en or zh-CN
}

service IPService {
list<GeoInfo> queryGeoInfoByIP(1:list<string> ipList);

list<GeoInfo> queryGeoInfo(1:GeoRequest req);

list<GeoInfo> queryI18nGeoInfoByIP(1:list<string> ipList);
}

业务逻辑说明

hook.go

描述:

定义并初始化了IP相关地理信息的原始数据结构体,并从文件当中读取所需地理信息数据,给其赋值,该结构体作为IP地理信息参照表,亦或查询表。

流程:

在加载配置文件之后,服务正式运行之前,通过New函数初始化该地理信息结构体;
然后,通过Open函数,读取配置里的路径信息,将该路径下文件里的地理信息通过信息的处理,依次读取到GeoModel的中文地理信息(cnGeos)和英文地理信息(enGeos)两个切片中,此时的GeoModel就成为了存储有完整IP相关地理信息的一张参照表;
GeoI18nModel类似,不过后者用作存储国际化的全球IP地理信息,没有中英文的区分,而是利用了命名为Names的Map基本结构扩展了所有所需的语言包,所以无需特意进行语言的区分,查询时打上语言标识即可。

IPservice.go

定义新建控制器。

main.go

描述:

定义并初始化了控制器及其进程,以及hook函数的定义以及运行。

Handler

描述:

IP_Service_Handler 功能
queryGeoInfo_Handler 通过发来的请求里包含的IP地址,参照GeoModel查询对应的地理信息; 如果查询不到或地理位置不在中国,则交给GeoI18nModel来继续查询对比相关的地理信息。 进行合适的日志处理之后,打包返回相关地理信息以及错误信息。
queryGeoInfoByIP_Handler 与上面不同的是,输入当中直接为IP_list,并没有相关的中英文标识,语言统一预设为中文识别, 当查询到地理信息不是中国的时候,则交给GeoI18nModel来继续查询对比相关的地理信息。
queryI18nGeoInfoByIP_Handler 与第二条相同,输入也直接为IP_list,不同的是,查询语言预设为英文。 并且是国际IP地理信息的查询服务,所以并不区分是否在“中国”国内。

流程:

见Model里的业务逻辑描述。

Model

  • geo_model

定义了Geo和GeoModel结构体。及其New函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//Geo定义了一个地区的IP区间
type Geo struct {
startIp uint32
endIp uint32
countryName string
provinceName string
cityName string
districtName string
carrierName string
}

//GeoModel定义了中英文地理信息的数据整体信息,包括了数据路径以及所有中英文地理信息完整的数据切片
type GeoModel struct {
enDataPath string
cnDataPath string
cnGeos []*Geo
enGeos []*Geo
}
  • open函数用来从文件中读取IP地理信息到geoModel结构体。该函数在hook.go中调用,用来初始化参照表GeoModel。
  • charToLong和charTolittle函数用来将IP地址转化为整数(ipValue),好进行区间比较,以便找到IP的归属地。
  • search函数和compare函数结合起来,找到ipValue的对应区间,用来查找IP的归属地。
  • GetInfoByIP函数则整合以上代码,进行查表,将一个IP地址信息,和其语言标识,来查询对应的地理信息表单,返回一个地理信息和相关错误信息。
  • geoi18n_model

定义了Geo和GeoModel结构体。及其New函数。

1
2
3
4
5
//GeoI18nModel定义了国际IP地理信息数据包,包括了数据路劲以及所有的IP地理信息数据
type GeoI18nModel struct {
dataPath string
Data *geoip2.Reader
}
  • open函数用来从文件中读取IP地理信息到GeoI18nModel结构体。该函数在hook.go中调用,用来初始化参照表GeoI18nModel。
  • GetI18nInfoByIP函数,进行查表操作,查询国际IP地理信息表单,将一个IP地址信息进行解析,然后查询IP地址的所属城市信息,然后依次赋值给需要返回的单个地理信息,最好返回该地理信息和相关错误信息。

重构项目

对这个项目进行重构,数据存储层迁移到 PG, 功能上支持 ipv6

数据存储层迁移到PgSQL

  • PostgreSQL数据存储设计方案

此处数据存储层指的地理位置信息的参照表,原本是以data形式存储在代码包文件当中的,如下。

data_en

需要将该文件存储到PG中,以查表的方式来查询IP_geoInfo。

所以需要设计两张表来分别存储这两个文件。命名分别以ip_service_cn/en命名。

设计结果如下:

其中注意

  • PG设置自增语句:
1
2
nextval('ip_service_cn_id_seq'::regclass)
nextval('ip_service_en_id_seq'::regclass)
  • PG插入多个数据的语句
1
INSERT INTO public.ip_service ( start_ip , end_ip , country_name , province_name , city_name , district_name , carrier_name) VALUES (16777216,16777471,'澳大利亚','','','',''),(16777472,16778239,'中国','福建','','','电信')
  • PG恢复自增字段序号为1的语句
1
2
3
4
TRUNCATE TABLE ip_service_en;
TRUNCATE TABLE ip_service_cn;
ALTER SEQUENCE ip_service_cn_id_seq RESTART WITH 1;
ALTER SEQUENCE ip_service_en_id_seq RESTART WITH 1;

因为查询IP_GEOINFO时,最主要是通过比对ip所处的区间,来得知,查询的IP属于哪一个地址范围,且,start_ip,end_ip有明显的连续性,所以可以由此建立索引。建表语句:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
CREATE TABLE "public"."ip_service_en"(
"id" serial8 NOT NULL PRIMARY KEY,
"create_time" timestamptz(6) NOT NULL DEFAULT now(),
"update_time" timestamptz(6) NOT NULL DEFAULT now(),
"start_ip" int8 NOT NULL DEFAULT 0,
"end_ip" int8 NOT NULL DEFAULT 0,
"country_name" varchar(100) NOT NULL DEFAULT '',
"province_name" varchar(100) NOT NULL DEFAULT '',
"city_name" varchar(100) NOT NULL DEFAULT '',
"district_name" varchar(100) NOT NULL DEFAULT '',
"carrier_name" varchar(100) NOT NULL DEFAULT ''
);
CREATE INDEX "idx_iprange_en" ON "public"."ip_service_en" (start_ip,end_ip);
CREATE TABLE "public"."ip_service_cn"(
"id" serial8 NOT NULL PRIMARY KEY,
"create_time" timestamptz(6) NOT NULL DEFAULT now(),
"update_time" timestamptz(6) NOT NULL DEFAULT now(),
"start_ip" int8 NOT NULL DEFAULT 0,
"end_ip" int8 NOT NULL DEFAULT 0,
"country_name" varchar(100) NOT NULL DEFAULT '',
"province_name" varchar(100) NOT NULL DEFAULT '',
"city_name" varchar(100) NOT NULL DEFAULT '',
"district_name" varchar(100) NOT NULL DEFAULT '',
"carrier_name" varchar(100) NOT NULL DEFAULT ''
);
CREATE INDEX "idx_iprange_cn" ON "public"."ip_service_cn" (start_ip,end_ip);
  • 数据迁移

因为涉及到的数据量有近五百万条IP地址段,所以需要考虑插入的效率问题。于是结合了两个方案:

  • 开Go程同时往中英文IP表单中插数据
1
2
3
4
5
6
7
var wg sync.WaitGroup
wg.Add(1)
go NewInsert(p, "en", &wg)
wg.Add(1)
go NewInsert(p, "cn", &wg)
wg.Wait()
Db.Close()
  • 结合sql语句,1000条数据提交一次。(见上文PG插入多个数据的语句格式)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
func NewInsert(p *GeoModel, tableName string, wg *sync.WaitGroup) {

var data, sql, sqlHead, sqlData, oneData string
sqlHead = "INSERT INTO public.ip_service_%s ( start_ip , end_ip , country_name , province_name , city_name , district_name , carrier_name)"
sqlData = "(%v,%v,'%v','%v','%v','%v','%v')"

if tableName == "en" {

for k, vousmevoyez := range p.enGeos {

if vousmevoyez.StartIP != 0 && vousmevoyez.EndIP != 0 {

oneData = fmt.Sprintf(sqlData, v.StartIP, v.EndIP, v.CountryName, v.ProvinceName, v.CityName, v.DistrictName, v.CarrierName)

if k%1000 != 0 && k < len(p.cnGeos) {
data += oneData
data += ","
} else {
data += oneData
sql = sqlHead + " VALUES " + data
fmt.Println(tableName + "***********:**********" + strconv.Itoa(k))
_, err := Db.Exec(sql)
if err != nil {
fmt.Println(sql, err)
TableHandler(0)
return
}
}
}

if k%1000 == 0 {
data = ""
}
}
} else {
//···略
}
wg.Done()
return
}

其间碰到的一个坑:在英文IP地理信息中,名称中有的时候有单引号如Yi'an City这个时候放在sql语句中就会报错,这个时候,如果想要在数据库中正确插入单引号,就要把一个单引号变成两个。所以,增加了一个排查单引号的功能,例子如下:

1
2
3
4
5
6
if len(tokens) >= 3 {
geo.CountryName = tokens[2]
if strings.Contains(geo.CountryName, "'") {
geo.CountryName = strings.Replace(geo.CountryName, "'", "''", -1)
}
}
  • 内部代码修改

后续只需要将源代码中关于geoModel的结构体做一次重新的适配,搜索的入口做一个重定向就行了,这里不做赘述。

如何支持IPV6

由于Leader说了,I18N的重构暂时不需要管IPV6的模块,所以就不动,并且,现阶段只需要做出一个能用的IPV6地址查询即可,于是,我首先要下载数据源,然后做地理查询前,同样要先将数据源转移到PG,然后再以类似的逻辑做地理查询,问题的难点在于,数据如何处理,和如何转移数据,以及数据库如何设计等等。

  • 地理数据集的处理

IPV6地理信息数据集的特性