<<上篇 | 首页

陈爱云:打造坚如磐石的搜索架构 - 中生代技术 | 十条

对于一个在线系统而言,性能和稳定性是永远要追求的两个方向,如果是分布式系统,性能不够可以用机器来凑(当然这不是最好的方法,性能的提升不是本文的关注点,所以这里不对提升性能的方法赘述),但是稳定性不能靠机器来堆,并且机器越来越多可能会带来更多的稳定性的问题。做在线系统的同学应该会对墨菲定理感触特别深,如果系统中的某个模块可能会出错,那么它一定会出错。或许可以尝试把奶酪面包和猫绑一起,制作一个永动机:)

 

下面介绍爱奇艺搜索架构高可用之路的演进。爱奇艺搜索始于2011年,距今已有六个年头,在这六年间爱奇艺搜索的用户量和qps都呈指数增长,这也对稳定性提出了很高的挑战。前面我们说,任何一个系统都不可能做的完美,一定会出错,我们需要做的是出错后,把对用户的影响降到最低,让用户感知不到系统的故障。把系统做稳定,多活、降级、限流、扩容都必不可少,下面分别说说我们是怎么做的。

 

异地多活

曾有人开玩笑说挖掘机是互联网行业最恐怖的武器,挖掘机一铲子下去,可能就把机房的光缆电缆挖断,此时如果没有备份机房,那再完美的系统都无法对外服务了。

 

备份可以分为两种,一种是备用的机房平时是不接线上流量,主机房出问题后再把流量切到备用机房上。这样做需要注意几点,一是备用机房的数据和程序版本要保持与主机房一致,二是备用机房平时没有接线上流量,在线上真出问题时,是否敢把线上流量切到备用机房上去,备用机房此时是否可以正常服务,三是备用机房的机器平时不在线上服务,也在一定程度上带来资源的浪费。另外一种备份方式是备用机房平时也在线上服务,一个机房出问题后可以把流量打向其余的机房。假设说有三个机房,那每个机房冗余1/3的机器即可,这样可以接受任意一个机房挂掉。爱奇艺采用的是后一种方式,采取离线数据多写的方式,在线读本机房的数据。

 

异地多活主要还是在数据一致性上比较难处理,搜索在线系统对数据一致性要求不是非常非常高,所以在线方面在实现异地多活困难并不多。

 

降级

降级可以通过各种手段降低系统的负载,去掉一些锦上添花的功能,保证基本的服务质量。基本的降级可以让用户几乎无感知的情况下降低系统的负载。

 

1. 延长缓存过期时间

众所周知,增加缓存可以大大降低后端模块的负载,增加缓存过期时间可以提高缓存命中率,同样可以降低打向后端模块的流量。但是如果缓存时间固定不变且比较长的话,后端数据更新后就不能及时体现在前端,如果缓存时间固定不变且比较短的话,缓存命中率比较低,对后端模块起不到太大的降低流量的作用。所以缓存过期时间要随着后端模块的压力而动态变化,那么,如何实现动态控制缓存过期时间的目的呢?

 

首先在写缓存时,把写缓存的时间戳也加进去,假设这个时间戳为t1,从缓存系统中取出缓存后,同时取得t1,假设当前时间为t2, t = t2 - t1,同时系统中维护一个过期时间的阈值,让t与这个阈值比较,如果t小于阈值,则认为缓存足够新,反之则认为缓存过期。这个阈值不是固定不变的,随着后端压力和后端的成功率而变化。

 

如果取到的缓存已过期,那么去请求后端模块,如果请求后端模块失败了,可以返回过期的缓存的内容。因为对于视频搜索来说,返回一个稍微旧点的结果,要远远好于不返回结果。

 

2. 降低计算复杂度

先举一个上学考试的例子,大部分同学考到80分相对比较容易,但是如果想要考到100分,需要为提高的20分花费大量的精力。可能花20%的精力可以达到80分,但是要再花80%的精力才能再提高20分。搜索质量的提高也如此。

 

可以通过缩小索引的数据范围来降低计算的复杂度。在100亿个doc中进行检索与在100万个doc中进行检索,所消耗的时间都不是一个量级的,所以可以通过缩小索引的数据范围来减少搜索消耗的CPU和时间。这里的范围也不是随便缩小的,需要保证重点数据依旧在索引范围内,可以通过文档的点击数、上线时间等来确定是否属于重点数据。

 

还可以通过省略部分大量消耗CPU的步骤来达到降低计算复杂度的目的,这些步骤就相当于我们前边所说的为了最后的20分而额外消耗了80%的精力,对于爱奇艺搜索来说,这部分是重排序,由于之前已经有粗排序了,去掉重排序依旧可以达到一个基本满意的搜索结果,同时消耗的CPU也很低。

 

这里所说的缩小数据集的范围与去掉重排序的步骤,可以是人为操作的,也可以是自动触发的。比如根据该进程所消耗的CPU,一旦达到某一阈值,则自动进行降级。

 

3. LIFO

很多系统都有一个任务队列,有一个IO线程负责往任务队列里push任务,工作线程从队列里pull任务,默认情况下,这个队列是FIFO的,但是设想这种情况,当工作线程处理不及时时,队列里的任务会越积越多,导致每一个任务都需要在队列里等待很长时间才会被得到处理,但是由于任务在队列里等待了过长的时间,当任务处理完后,可能client已经认为本次请求超时了,server做了无用功,极端情况下可能会导致server一直在做无用功。

 

所以在需要降级时,可以改变任务队列取任务的方式,改为LIFO,起码可以保证新任务得以处理,旧任务如果过期则丢弃掉,有舍才有得嘛。因为任务队列中的任务是按照时间顺序放进去的,所以在LIFO时,一旦取出来一个任务已经过期,则意味着接下来取出的任务也是过期的,此时可以直接把队列清空,不需要挨个取出来再丢弃。

 

4. 缩短任务队列过期时间

为什么要给队列中的任务设置过期时间?因为防止任务已经在队列中等待了很长时间,防止取出执行完后任务已经超时。比如说从队列中取出任务后,得知任务已经在队列中等待了90ms,而此时处理一个任务平均需要消耗20msclient设置的超时时间是100ms,那么就可以大概率的得知这个任务处理完后,client已经认为该任务超时了,那么就没必要再消耗CPU继续处理。所以设置一个任务队列的过期时间是必要的。

 

为什么要动态调整队列中任务的过期时间呢?正如我们前边所说,任务的过期时间是跟当前处理任务的平均耗时相关的,而当前处理任务的平均时间不是一成不变的,会随着当前机器的各种资源的情况发生变化。当任务平均处理时间比较短时,可以容忍任务在队列中多等一会儿,而当任务平均处理时间比较长时,只能允许任务在队列中停留较短的时间。

 

限流

相比降级,限流会直接让部分用户的请求不会被处理,会在一定程度上影响用户体验,是有损的。但是如果经过降级,还不能保证系统的负载处于一个安全的范围内,就需要限流了。限流属于舍小为大,拒绝一部分用户的请求,保证整个系统可用,而不是不顾自身实力蛮力处理所有请求,导致系统挂掉,最终一个请求都处理不了。

 

限流又可以分为两种,分为server根据自身处理能力主动丢弃部分请求,和client根据server的平均响应时间和server的成功率减少给server发的请求的数量。

 

限流的维度可以有多个,根据程序本身的指标:连接数、总的qps、分类的qpsserver一般要对接不同的端,不能因为某一个端的流量上涨而影响到其他端的流量。也可以根据机器的总体指标来进行限流,比如说网络流量、CPU、内存等。

 

扩容

如果经过降级、限流后,系统的负载依旧不能维持在一个相对安全的范围内,此时说明我们现有的资源已经不足以满足用户热情的需求了,此时就需要扩容了。

 

扩容可以使用docker自动完成,根据请求的平均响应时间、请求的成功率、机器的负载等自动判断和扩容,也可以根据过去一天、一周、一个月、一年的流量,来提前预估接下来的流量趋势,提前做出扩容和缩容。

 

经过上述异地多活、降级、限流、扩容等措施,可以保证系统不出大问题,但是优化的道路任重道远,要持续不断地优化才能保证系统越来越稳定。也希望我们的经验教训能帮到大家,让大家再也不用担心各种异常各种突然状况。(全文完)

阅读全文……

标签 : ,

Fix certificate problem in HTTPS - Real's Java How-to

HTTPS protocol is supported since JDK1.4 (AFAIK), you have nothing special to do.

import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URL;
import java.net.URLConnection;

public class ConnectHttps {
  public static void main(String[] args) throws Exception {
    URL url = new URL("https://securewebsite.com");
    URLConnection con = url.openConnection();
    Reader reader = new InputStreamReader(con.getInputStream());
    while (true) {
      int ch = reader.read();
      if (ch==-1) {
        break;
      }
      System.out.print((char)ch);
    }
  }
}

However, you can have a problem if the server certificate is self-signed by a testing certification authority (CA) which is not in trusted CAs of Java on the client side. An exception like

Exception in thread "main" javax.net.ssl.SSLHandshakeException:
  sun.security.validator.ValidatorException:
    PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException:
      unable to find valid certification path to requested target

is thrown. This is a common situation with a development server.

The fix is to add the self signed certificate to trusted CAs on the client side. You do that by updating the CACERT file in the your JRE_HOME/lib directory.

Check this tutorial : http://www.java-samples.com/showtutorial.php?tutorialid=210

Or you can override the check and accept an untrusted certificate (with the risk coming with it!).

import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URL;
import java.net.URLConnection;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.security.cert.X509Certificate;

public class ConnectHttps {
  public static void main(String[] args) throws Exception {
    /*
     *  fix for
     *    Exception in thread "main" javax.net.ssl.SSLHandshakeException:
     *       sun.security.validator.ValidatorException:
     *           PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException:
     *               unable to find valid certification path to requested target
     */
    TrustManager[] trustAllCerts = new TrustManager[] {
       new X509TrustManager() {
          public java.security.cert.X509Certificate[] getAcceptedIssuers() {
            return null;
          }

          public void checkClientTrusted(X509Certificate[] certs, String authType) {  }

          public void checkServerTrusted(X509Certificate[] certs, String authType) {  }

       }
    };

    SSLContext sc = SSLContext.getInstance("SSL");
    sc.init(null, trustAllCerts, new java.security.SecureRandom());
    HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());

    // Create all-trusting host name verifier
    HostnameVerifier allHostsValid = new HostnameVerifier() {
        public boolean verify(String hostname, SSLSession session) {
          return true;
        }
    };
    // Install the all-trusting host verifier
    HttpsURLConnection.setDefaultHostnameVerifier(allHostsValid);
    /*
     * end of the fix
     */

    URL url = new URL("https://securewebsite.com");
    URLConnection con = url.openConnection();
    Reader reader = new InputStreamReader(con.getInputStream());
    while (true) {
      int ch = reader.read();
      if (ch==-1) {
        break;
      }
      System.out.print((char)ch);
    }
  }
}
 

阅读全文……

标签 : ,

爬取百度网盘用户分享 | Guodong

上面3个连接请求必须带上 ("Referer", "https://yun.baidu.com/share/home?uk=23432432#category/type=0"),uk多少无所谓,否则请求不到json数据,
获取用户订阅和获取用户粉丝每次请求一次休眠2s的话可以无限制请求,对ip没要求,获取用户分享超坑,一个ip只能请求10次,并且休眠也没用.
因为没有那么多ip,我就去研究手机版的用户分享,手机版获取用户分享可以一次性连续请求60次,60次后必须休眠35s左右在继续请求就可以,不会像pc版那样必须换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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
private Logger log = LoggerFactory.getLogger(FollowStartIndex.class);
public void startIndex() {
//无限循环
while (true) {
//从数据库获取可用uk,可用首先从某个粉丝超多的用户入手,获取他粉丝的uk,存入数据库
Avaiuk avaiuk = Avaiuk.dao.findFirst("select * from avaiuk where flag=0 limit 1");
//更新数据库,标记该uk已经被用户爬过
avaiuk.set("flag", 1).update();
getFllow(avaiuk.getLong("uk"), 0);
}
}

static String url = "http://yun.baidu.com/pcloud/friend/getfollowlist?query_uk=%s&limit=24&start=%s&bdstoken=e6f1efec456b92778e70c55ba5d81c3d&channel=chunlei&clienttype=0&web=1&logid=MTQ3NDA3NDg5NzU4NDAuMzQxNDQyMDY2MjA5NDA4NjU=";
static Map map = new HashMap();

static {
map.put("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.116 Safari/537.36");
map.put("X-Requested-With", "XMLHttpRequest");
map.put("Accept", "application/json, text/javascript, */*; q=0.01");
map.put("Referer", "https://yun.baidu.com/share/home?uk=325913312#category/type=0");
map.put("Accept-Language", "zh-CN");
}

//获取订阅用户
public void getFllow(long uk, int start, boolean index) {
log.info("进来getFllow,uk:{},start:{}", uk, start);
boolean exitUK = false;
try {
exitUK = Redis.use().exists(uk);
} catch (Exception e) {
exitUK = true;
}
if (!exitUK) {
Redis.use().set(uk, "");
if (index) {
indexResource(uk);
}
recFollow(uk,start,true);
} else {
if (start > 0) {//分页订阅
recFollow(uk,start,false);
} else {
log.warn("uk is index:{}", uk);
}
}


}
public void recFollow(long uk,int start,boolean goPage){
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
String real_url = String.format(url, uk, start);
ResponseBody body = OkhttpUtil.syncGet(real_url, Headers.of(map));
if (body != null) {
try {
Follow follow = JSON.parseObject(body.string(), Follow.class);

if (follow.getErrno() == 0) {
List<Follow.FollowListBean> followListBeen = follow.getFollow_list();
if (followListBeen != null && followListBeen.size() > 0) {
log.info("不为空:{}", follow);
for (Follow.FollowListBean bean : followListBeen) {
int follow_count = bean.getFollow_count();
int shareCount=bean.getPubshare_count();
if (follow_count > 0) {
if (shareCount > 0) {
getFllow(bean.getFollow_uk(), 0, true);
} else {
getFllow(bean.getFollow_uk(), 0, false);
}
}
}
if(goPage){
int total_count = follow.getTotal_count();
//log.warn("分页页数:{}",total_count);
//分页
int total_page = (total_count - 1) / 24 + 1;

for (int i = 1; i < total_page; i++) {
getFllow(uk, i * 24,false);
}

}
} else {
log.info("为空:{}", follow);
}
}

} catch (IOException e) {
e.printStackTrace();
}
}
}

long uinfoId = 0;
long nullStart = System.currentTimeMillis();

public void indexResource(long uk) {
while (true) {
String url = "http://pan.baidu.com/wap/share/home?uk=%s&start=%s&adapt=pc&fr=ftw";
String real_url = String.format(url, uk, 0);

YunData yunData = DataUtil.getData(real_url);

if (yunData != null) {
log.info("{}", yunData.toString());
int share_count = yunData.getUinfo().getPubshare_count();
if (share_count > 0) {
Uinfo uinfo = new Uinfo();
uinfo.set("uname", yunData.getUinfo().getUname()).set("avatar_url", yunData.getUinfo().getAvatar_url()).set("uk", uk).set("incache", 1).save();
uinfoId = uinfo.getLong("id");
List<Records> recordses = yunData.getFeedata().getRecords();
for (Records record : recordses) {
new ShareData().set("title", record.getTitle()).set("shareid", record.getShareid()).set("uinfo_id", uinfoId).save();
}

}
int totalPage = (share_count - 1) / 20 + 1;

int start = 0;
if (totalPage > 1) {
for (int i = 1; i < totalPage; i++) {
start = i * 20;
real_url = String.format(url, uk, start);
yunData = DataUtil.getData(real_url);
if (yunData != null) {
log.info("{}", yunData.toString());
List<Records> recordses = yunData.getFeedata().getRecords();
for (Records record : recordses) {
//用户分享的数据存入数据库
new ShareData().set("title", record.getTitle()).set("shareid", record.getShareid()).set("uinfo_id", uinfoId).save();
}
} else {
i--;
log.warn("uk:{},msg:{}", uk, yunData);
long temp = nullStart;
nullStart = System.currentTimeMillis();
if ((nullStart - temp) < 1500) {
try {
Thread.sleep(60000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

}

}

}
break;
} else {
log.warn("uk:{},msg:{}", uk, yunData);
long temp = nullStart;
nullStart = System.currentTimeMillis();
//在1500毫秒内2次请求到的数据都为null时,此时可能被百度限制了,休眠一段时间就可以恢复
if ((nullStart - temp) < 1500) {
try {
Thread.sleep(60000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}


}
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
public class DataUtil {
public static YunData getData(String url) {
//自己对okhttp的封装
ResponseBody body = OkhttpUtil.syncGet(url);
String html = null;
if (body == null) {
return null;
}
try {
html = body.string();
} catch (IOException e) {
return null;
}
Pattern pattern = Pattern.compile("window.yunData = (.*})");
Matcher matcher = pattern.matcher(html);
String json = null;
while (matcher.find()) {
json = matcher.group(1);
}
if (json == null) {
return null;
}
//fastjson
YunData yunData = JSON.parseObject(json, YunData.class);
return yunData;
}
}

YunData自己获取下json数据,就能创建出来,代码就不放了.

这么爬取速度很快,3台服务器一天就爬取了100多万.
Golang版本

阅读全文……

标签 : , ,