IPv6定位優(yōu)化

背景
隨著 IPv6的推進(jìn),我們發(fā)現(xiàn)線(xiàn)上需要使用 IPv6 定位的流量已經(jīng)達(dá)到了 8000 QPS。此前我們并未對(duì) IPv6 定位做任何緩存或者其它優(yōu)化,這部分流量會(huì)直接請(qǐng)求定位服務(wù),隨著流量進(jìn)一步提升可能觸發(fā)調(diào)用量報(bào)警以及流控。另外由于此前已經(jīng)對(duì) IPv4 進(jìn)行了緩存,如果 IPv6 不做相應(yīng)的優(yōu)化,因?yàn)槎嗔艘淮?RPC 請(qǐng)求,服務(wù)的響應(yīng)時(shí)間會(huì)隨著 IPv6 流量占比提升而變長(zhǎng)。
調(diào)研
通過(guò)和定位服務(wù)負(fù)責(zé)人溝通,我們獲取到如下有用信息:
IPv6 定位數(shù)據(jù)是從外部采購(gòu),數(shù)據(jù)量大概是幾十萬(wàn)條 和 IPv4 類(lèi)似,前綴相同的地址定位到相同的地域,但是不像 IPv4 使用固定的前3段,具體多少位不確定,由另外一個(gè)參數(shù)決定 數(shù)據(jù)每天更新一次,每次變更量不大,幾百條左右 除了提供定位服務(wù)之外,還會(huì)將定位數(shù)據(jù)在 HDFS 上存儲(chǔ)一份
定位數(shù)據(jù)示例如下:
2001:250:200::/48 中國(guó) 北京市 北京
2001:250:201::/48 中國(guó) 北京市 北京
2001:250:202::/48 中國(guó) 北京市 北京
2001:250:203::/48 中國(guó) 北京市 北京
2001:250:204::/48 中國(guó) 北京市 北京
使用第一行來(lái)進(jìn)行一個(gè)說(shuō)明,如果地址的前 48 位和 2001:250:200:: 的前 48 位一致,則定位到北京。另外需要注意的是雖然示例數(shù)據(jù)中都是根據(jù)前 48 位來(lái)進(jìn)行定位,但是這個(gè)值可能是不同的。
方案
從定位數(shù)據(jù)的格式以及定位邏輯來(lái)看,比較適合的數(shù)據(jù)結(jié)構(gòu)是前綴樹(shù),這樣可以很好的實(shí)現(xiàn)根據(jù)前 xx 位進(jìn)行定位。IPv6 共有 128位,即 128 個(gè) 0 和 1,由于值要么是 0 要么是 1,所以構(gòu)建出來(lái)的是一顆二叉樹(shù),數(shù)據(jù)結(jié)構(gòu)相關(guān)的代碼如下:
private Node root = new Node();
public class Node {
private Node[] children = new Node[2];
private Integer localId = null;
public Integer getLocalId() {
return localId;
}
public void setLocalId(Integer localId) {
this.localId = localId;
}
public Node getChild(int pos) {
return children[pos];
}
public void setChild(int pos, Node child) {
children[pos] = child;
}
}
比如數(shù)據(jù) 2001:250:200::/48 只需要使用到前 48 位即可,并且在第 48 位上標(biāo)記地域信息,構(gòu)建前綴樹(shù)的代碼如下:
public void put(Inet6Address inet6Address, Integer mask, Integer localId) {
if (inet6Address == null || localId == null || localId <= 0 || mask == null || mask <= 0 || mask > 128) {
return;
}
Address address = new Address(inet6Address);
Node node = root;
for (int i = 0; i < mask; i++) {
int pos = address.valueAt(i);
Node child = node.getChild(pos);
if (child == null) {
child = new Node();
node.setChild(pos, child);
}
node = child;
}
node.setLocalId(localId);
}
public static class Address {
private static int[] temp = new int[]{0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x1};
private byte[] bytes;
public Address(Inet6Address address) {
this.bytes = address.getAddress();
}
public byte valueAt(int pos) {
if (pos < 0 || pos >= 128) {
throw new IllegalArgumentException("pos 的取值范圍是 [0, 128)");
}
int num = pos / 8;
int p = pos % 8;
return (byte) ((bytes[num] & temp[p]) >>> (7 - p));
}
}
其中 Address 類(lèi)是對(duì) JDK 內(nèi)置類(lèi) Inet6Address 的一個(gè)封裝,提供了便捷的取指定位的方法 valueAt。
通過(guò)上述代碼使用定位數(shù)據(jù)的每一行調(diào)用 put 方法即可完成前綴樹(shù)的構(gòu)建,下邊看下構(gòu)建好的前綴樹(shù)如何進(jìn)行查找:
public Integer get(Inet6Address inet6Address) {
if (inet6Address == null) {
return null;
}
Address address = new Address(inet6Address);
Node node = root;
for (int i = 0; i < 128; i++) {
int pos = address.valueAt(i);
Node child = node.getChild(pos);
if (child == null) {
return null;
}
Integer localId = child.getLocalId();
if (localId != null) {
return localId;
}
node = child;
}
return null;
}
通過(guò)上述代碼已經(jīng)可以實(shí)現(xiàn)對(duì)前綴樹(shù)進(jìn)行構(gòu)建以及查找,需要注意這顆前綴樹(shù)是不能進(jìn)行修改的,只能新增和查找。
服務(wù)啟動(dòng)的時(shí)候需要進(jìn)行前綴樹(shù)的初始化,此時(shí)會(huì)請(qǐng)求 HDFS 拉取定位數(shù)據(jù),由于網(wǎng)絡(luò)請(qǐng)求不總是靠譜的,增加了三次重試,另外在鏡像中放置一份數(shù)據(jù)(更新頻率更低)來(lái)進(jìn)行降級(jí),避免服務(wù)啟動(dòng)失敗。
對(duì)于數(shù)據(jù)更新,使用了一個(gè)定時(shí)任務(wù)每天 8 點(diǎn)更新,因?yàn)槎ㄎ粩?shù)據(jù)每天變更的量很小,這個(gè)更新是允許失敗的,更新的時(shí)候會(huì)根據(jù)新的定位數(shù)據(jù)構(gòu)建一顆前綴樹(shù),如果更新成功則替換之前的前綴樹(shù),更新結(jié)果會(huì)記錄一條日志,后續(xù)只需要關(guān)注下相關(guān)日志即可,不再做一個(gè)很復(fù)雜的方案來(lái)保證更新成功。
通過(guò)上述方案即可處理好 IPv6 的定位,同時(shí)由于不使用 RPC 調(diào)用,也會(huì)給性能和響應(yīng)時(shí)間帶來(lái)一定的提升。
- END -