使用Lua编写插件加强Nginx - 初探

虽然说 Nginx 的标准模块非常强大,然而如果遇到了一些不能灵活适应系统要求的功能时候,我们往往会考虑使用 Lua 拓展和定制 Nginx 服务,目前对于这种需要拓展性的项目一般采用 OpenResty ,按照官方的介绍,OpenResty 是一个基于 Nginx 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态Web 应用、Web 服务和动态网关 但是,有的时候服务已经部署完成,包括配置都已经写好了,直接对线上的 Nginx 进行替换显然是一种非常冒险与费神的行为,又或者是只需要某一项功能的拓展,并不需要整体做大改动,这个时候用 Lua 模块就是一种很好的选择。 模拟线上环境 这里我启用了虚拟机作为线上的机器,因此需要重新安装 Nginx,Arguments 这边是需要单独记下来的,和后面编译做到尽量一个参数,这样对配置文件不会造成任何影响 $ sudo add-apt-repository ppa:nginx/stable $ sudo apt-get update $ sudo apt install nginx $ nginx -V nginx version: nginx/1.18.0 (Ubuntu) built with OpenSSL 1.1.1f 31 Mar 2020 TLS SNI support enabled configure arguments: --with-cc-opt='-g -O2 -fdebug-prefix-map=/build/nginx-d4S6DH/nginx-1.18.0=. -fstack-protector-strong -Wformat -Werror=format-security -fPIC -Wdate-time -D_FORTIFY_SOURCE=2' --with-ld-opt='-Wl,-Bsymbolic-functions -Wl,-z,relro -Wl,-z,now -fPIC' --prefix=/usr/share/nginx --conf-path=/etc/nginx/nginx.conf --http-log-path=/var/log/nginx/access.log --error-log-path=/var/log/nginx/error.log --lock-path=/var/lock/nginx.lock --pid-path=/run/nginx.pid --modules-path=/usr/lib/nginx/modules --http-client-body-temp-path=/var/lib/nginx/body --http-fastcgi-temp-path=/var/lib/nginx/fastcgi --http-proxy-temp-path=/var/lib/nginx/proxy --http-scgi-temp-path=/var/lib/nginx/scgi --http-uwsgi-temp-path=/var/lib/nginx/uwsgi --with-compat --with-debug --with-pcre-jit --with-http_ssl_module --with-http_stub_status_module --with-http_realip_module --with-http_auth_request_module --with-http_v2_module --with-http_dav_module --with-http_slice_module --with-threads --with-http_addition_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_sub_module 编译包含了 Lua 模块的 Nginx $ mkdir nginx && cd nginx $ wget https://nginx.org/download/nginx-1.18.0.tar.gz $ tar xvf nginx-1.18.0.tar.gz 不推荐去 LuaJIT 官网下载 2.0.5 版本,不然在 arm64 下将无法进行编译(M1 Mac) ...

February 23, 2022 · 3 min · Sxueck

从GoProxy中学习反向代理

阅读目的 弄清楚项目是如何完成代理的功能,因此重点弄懂 Proxy 部分的代码 项目地址 https://github.com/goproxyio/goproxy 从项目启动开始,我们的重点就是下面的这个参数 flag.StringVar(&proxyHost, "proxy", "", "next hop proxy for Go Modules, recommend use https://gopropxy.io") 我们将这个参数设置为国内可以访问的 https://goproxy.cn ,并且尝试一下是否能跑通整个服务,这里使用的是 Powershell,换成 Linux Shell 的时候其实也是一样的流程 # 单独开启一个终端启动服务 $ $CacheDIR = "G:\\GomodDebug" $ mkdir -p $CacheDIR\pkg\mod\cache\download\github.com\goproxyio\goproxy\@v\ $ go run . -proxy https://goproxy.cn -listen 127.0.0.1:8085 -cacheDir $CacheDIR goproxy.io: ProxyHost https://goproxy.cn goproxy.io: ------ --- /github.com/@v/list [proxy] goproxy.io: ------ --- /github.com/goproxyio/goproxy/@v/list [proxy] goproxy.io: ------ --- /github.com/goproxyio/@v/list [proxy] goproxy.io: 0.344s 404 /github.com/@v/list goproxy.io: 0.548s 200 /github.com/goproxyio/goproxy/@v/list goproxy.io: 0.619s 404 /github.com/goproxyio/@v/list goproxy.io: ------ --- /github.com/goproxyio/goproxy/internal/@v/list [proxy] goproxy.io: ------ --- /github.com/goproxyio/goproxy/internal/cfg/@v/list [proxy] goproxy.io: ------ --- /github.com/goproxyio/goproxy/internal/module/@v/list [proxy] goproxy.io: ------ --- /github.com/goproxyio/goproxy/internal/modfetch/@v/list [proxy] goproxy.io: ------ --- /github.com/goproxyio/goproxy/internal/modfetch/codehost/@v/list [proxy] goproxy.io: ------ --- /github.com/goproxyio/goproxy/internal/modload/@v/list [proxy] goproxy.io: 0.428s 404 /github.com/goproxyio/goproxy/internal/@v/list goproxy.io: 0.473s 404 /github.com/goproxyio/goproxy/internal/module/@v/list goproxy.io: 0.532s 404 /github.com/goproxyio/goproxy/internal/modfetch/@v/list goproxy.io: 0.555s 404 /github.com/goproxyio/goproxy/internal/modload/@v/list goproxy.io: 0.592s 404 /github.com/goproxyio/goproxy/internal/cfg/@v/list goproxy.io: 0.697s 404 /github.com/goproxyio/goproxy/internal/modfetch/codehost/@v/list # 这是IDE中正常的测试终端 $ go env -w GO111MODULE=on $ go env -w GOPROXY=http://127.0.0.1:8085 # 我们上面并没有设置direct,意味着拉取源只会通过代理 $ go get -u github.com/goproxyio/goproxy github.com/goproxyio/goproxy imports github.com/goproxyio/goproxy/pkg/proxy imports ... 深入研究Proxy的流程 Director handle = &logger{proxy.NewRouter(proxy.NewServer(new(ops)), &proxy.RouterOptions{ Pattern: excludeHost, Proxy: proxyHost, DownloadRoot: downloadRoot, CacheExpire: cacheExpire, })} func NewRouter(srv *Server, opts *RouterOptions) *Router { rt := &Router{ opts: opts, srv: srv, } if opts != nil { if opts.Proxy == "" { log.Printf("not set proxy, all direct.") return rt } remote, err := url.Parse(opts.Proxy) if err != nil { log.Printf("parse proxy fail, all direct.") return rt } proxy := httputil.NewSingleHostReverseProxy(remote) director := proxy.Director proxy.Director = func(r *http.Request) { director(r) r.Host = remote.Host } rt.proxy = proxy rt.proxy.Transport = &http.Transport{ Proxy: http.ProxyFromEnvironment, TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, } rt.proxy.ModifyResponse = rt.customModResponse ... } return rt } 如果没有设置代理或者代理没有通过 url.Parse 的解析 ,服务将会提示将会使用直连模式 ...

February 11, 2022 · 4 min · Sxueck

如何编写一款网络代理服务

本篇文章仅供技术交流,请遵循相关国家法律法规,作者不提供任何技术支持 没啥思路,文章暂时搁置 - 2022-03-02 前言 Socks5 协议 在编写相关代码之前,我们需要先将 Socks5 协议给了解一下 使用 WireShark 抓取 Socks5 报文 $ curl --socks5 157.90.140.29:1080 ip.sb 同时我也将 WireShark 抓取到的报文片段截取下来了,可以点击 下载 后,使用相同的软件进行本地分析 分析Go-Shadowsocks2的源码 tcp.go // Create a SOCKS server listening on addr and proxy to server. func socksLocal(addr, server string, shadow func(net.Conn) net.Conn) { logf("SOCKS proxy %s <-> %s", addr, server) tcpLocal(addr, server, shadow, func(c net.Conn) (socks.Addr, error) { return socks.Handshake(c) }) } // Create a TCP tunnel from addr to target via server. func tcpTun(addr, server, target string, shadow func(net.Conn) net.Conn) { tgt := socks.ParseAddr(target) if tgt == nil { logf("invalid target address %q", target) return } logf("TCP tunnel %s <-> %s <-> %s", addr, server, target) tcpLocal(addr, server, shadow, func(net.Conn) (socks.Addr, error) { return tgt, nil }) } addr:客户端监听地址 server:客户端连接的地址,也就是远程服务器的地址 而关于 shadow 这个传入的匿名函数,我们可以通过其他地方是如何调用它的来进行作用判断 ...

January 24, 2022 · 2 min · Sxueck

经验杂谈

这里汇集了平常学习和工作中遇到的一些小小的疑难杂症 Linux Service Nginx xxx.so is not binary compatible in /etc/nginx/nginx.conf 这个通常发生在编译 Nginx 模块后,我们通过 load_module 加载该模块发出的报错,针对这种的解决方案也很简单,加上 --with-compat 即可,例如 ./configure --with-http_image_filter_module=dynamic --with-compat Kubernetes 数据库中间件问题 MongoDB ReplicaSetNoPrimary 问题 错误日志: server selection error: server selection timeout, current topology: { Type: ReplicaSetNoPrimary, Servers: [{ Addr: xxx:27017, Type: Unknown, Last error: connection() error occured during connection handshake: connection(xxx:27017[-127]) socket was unexpectedly closed: EOF }, { Addr: xxx:27017, Type: Unknown, Last error: connection() error occured during connection handshake: connection(xxx:27017[-128]) socket was unexpectedly closed: EOF }, ] } 从表面上看,代码 SDK 连接到了 Mongo 后,Mongo 返回来一个 ReplicaSet 集合,其中里面没有我可以访问的地址(返回了集群 POD 的 Name),其主要的原因是使用的 SDK 较老,那时候还没有集群化管理这种东西,不具备服务发现的功能 解决方案: In contrast to the mongo-go-driver, by default it would perform server discovery and attempt to connect as a replica set. If you would like to connect as a single node, then you need to specify connect=direct in the connection URI. 例如,在 PyMongo 的连接中加入 directConnection = True,或者类似方案 ...

January 24, 2022 · 4 min · Sxueck

服务器性能优化之网络性能优化

这篇文章,将会从网络原理开始,一直到 Linux Kernel 的相关配置对于网络性能的影响,算是一个偏整体的知识学习 文章非常长,这里将大纲列出来 网卡收发包队列技术优化 单网卡队列 RPS / RFS 原理 TCP Socket 建立连接相关内核参数优化 TCP 连接拥塞控制相关内核参数优化 TCP 拥塞控制算法 - BBRv2 原理 TCP 显式拥塞通知 ECN 算法原理 TCP 快速启动解决方案 概念梳理 学习过程中可能会碰到非常多的概念和术语,在此先对这些进行一个梳理,也算是一个复习 中断和软中断 首先我们需要对中断和软中断有个起码的认知,中断一般是指硬件中断,多由系统自身或与之链接的外设(如键盘、鼠标、网卡等)产生,中断首先是处理器提供的一种响应外设请求的机制,是处理器硬件支持的特性,一个外设通过产生一种电信号通知中断控制器,中断控制器再向处理器发送相应的信号。处理器检测到了这个信号后就会打断自己当前正在做的工作,转而去处理这次中断。而内核收到信号后会调用一个称为中断处理程序(interrupt handler)或中断服务例程(interrupt service routine)的特定程序。中断处理程序或中断服务例程可以在中断向量表中找到,这个中断向量表位于内存中的固定地址中。CPU处理中断后,就会恢复执行之前被中断的程序。 整个流程可以归纳为:硬件设备 (产生中断)-> 中断控制器(通知) -> CPU -> 中断内核 -> do_IRQ() -> handler_IRQ_event -> 中断控制器(收到恢复通知)-> IRQ_exit 现在我们对于基本的中断有了个理解,从实际场景来说中断请求的处理程序应该要短且快,这样才能减少对正常进程运行调度地影响,而且中断处理程序可能会暂时关闭中断,这时如果中断处理程序执行时间过长,可能在还未执行完中断处理程序前,会丢失当前其他设备的中断请求。 所以 Linux 系统为了解决中断处理程序执行过长和中断丢失的问题,将中断过程分成了两个阶段,分别是 上半部分 和 下半部分 上半部用来快速处理中断,一般会暂时关闭中断请求,主要负责处理跟硬件紧密相关或者时间敏感的事情。 下半部用来延迟处理上半部未完成的工作,一般以 内核线程 的方式运行。 例如网卡收包这里,网卡收到网络包后,会通过硬件中断通知内核有新的数据到了,于是内核就会调用对应的中断处理程序来响应该事件,这个事件的处理也是会分成上半部和下半部。 上部分要做到快速处理,所以只要把网卡的数据读到内存中,然后更新一下硬件寄存器的状态,比如把状态更新为表示数据已经读到内存中的状态值。接着,内核会触发一个软中断,把一些处理比较耗时且复杂的事情,交给「软中断处理程序」去做,也就是中断的下半部,其主要是需要从内存中找到网络数据,再按照网络协议栈,对网络数据进行逐层解析和处理,最后把数据送给应用程序。 所以,中断处理程序的上部分和下半部可以理解为: 上半部直接处理硬件请求,也就是硬中断,主要是负责耗时短的工作,特点是快速执行 下半部是由内核触发,也就说软中断,主要是负责上半部未完成的工作,通常都是耗时比较长的事情,特点是延迟执行 网卡收发过程 网卡的收发其实重点应该在中断的下半部,我们一拍脑袋,想到接收数据包的一个经典过程: 数据到达网卡 网卡产生一个中断给内核 内核使用I/O指令,从网卡I/O区域中去读取数据 但是这种方法有一个很大的问题,就是当大流量数据到来时候,网卡会产生大量的中断,内核在处理中断上下文中,会浪费大量资源来处理中断本身。所以,NAPI 技术被提出来,所谓的 NAPI 技术,其实就是先将内核屏蔽,然后每隔一段时间去轮询网卡是否有数据。不过相应的,如果数据量少,轮询本身也会占用大量不必要的 CPU 资源,所以需要进行抉择判断。 ...

January 22, 2022 · 10 min · Sxueck

编写一个带 SASL 认证的Kafka镜像

要求说明: Kafka Brokers 不管是互相通信,对外认证,还是和 Zookeeper 的交互进行,都需要使用 SASL/PLAIN 进行认证 思路: Kafka 的 bin 目录下面,包含了 Zookeeper 和 Kafka Server 的启动脚本,我们只需要在脚本运行之前,通过环境变量 KAFKA_OPTS 指定对应的 JAAS 认证文件,最后启动 properties 文件即可,理清了整个环节之后还是非常简单的 操作开始 目录结构 首先需要编写 Dockerfile 作为环境变量声明的地方,不然配置文件就没法使用模板进行编排了 FROM openjdk:8-jre-slim RUN set -eux; \ apt -y update && apt -y install procps; \ apt clean WORKDIR / ENV STORE_DATA="/kafka_2.12-1.1.1/store/data" \ STORE_LOGS="/kafka_2.12-1.1.1/store/logs" \ ZOO_USER="ZookeeperUsername" \ ZOO_PASS="ZookeeperPassword" \ ZOO_QUORUM_USER="QuorumUserName" \ ZOO_QUORUM_PASS="QuorumUserPassword" \ KAFKA_USER="KafkaUsername" \ KAFKA_PASS="KafkaPassword" \ KAFKA_CLIENT_BROKER_USER="KafkaClientBrokerUsername" \ KAFKA_CLIENT_BROKER_PASS="KafkaClientBrokerPassword" \ ZOOKEEPER_CONNECT="localhost:2181" \ MODE="" \ # create multiple users with different permissions # operation attribute: Read Write * # KAFKA_ACCOUNT_GROUP "user1|password|* readonlyuser|password|Read" KAFKA_ACCOUNT_GROUP="" \ # each pod contains a zk and a kafka # so their number of nodes and information should be the same CLUSTER_NODE="" # CLUSTERNODE "node0|0|127.0.0.1 node1|1|127.0.0.1" ENV PATH=$PATH:/kafka_2.12-1.1.1/bin \ ZOO_DATA_DIR="$STORE_DATA/zookeeper/data" \ ZOO_MYID_FILE="$STORE_DATA/zookeeper/data/myid" \ ZOO_DATA_LOG_DIR="$STORE_LOGS/zookeeper/logs/dataLogs" \ ZOO_LOG_DIR="$STORE_LOGS/zookeeper/logs" \ SERVER_JVMFLAGS="-Djava.security.auth.login.config=/kafka_2.12-1.1.1/config/zoo_server_jaas.conf" \ KAFKA_OPTS="-Djava.security.auth.login.config=/kafka_2.12-1.1.1/config/kafka_server_jaas.conf" \ ZOO_CFG="/kafka_2.12-1.1.1/config/zookeeper.properties" \ Z00_AUTH="/kafka_2.12-1.1.1/config/zoo_server_jaas.conf" \ KAFKA_CFG="/kafka_2.12-1.1.1/config/server.properties" \ KAFKA_AUTH="/kafka_2.12-1.1.1/config/kafka_server_jaas.conf" \ KAFKA_DATA_LOG_DIR="$STORE_LOGS/kafka/logs/dataLogs" ENV ENV_CONFIGRUE="ZOO_DATA_DIR ZOO_DATA_LOG_DIR ZOO_USER ZOO_PASS ZOO_QUORUM_USER ZOO_QUORUM_PASS KAFKA_USER KAFKA_PASS KAFKA_CLIENT_BROKER_USER KAFKA_CLIENT_BROKER_PASS KAFKA_DATA_LOG_DIR ZOOKEEPER_CONNECT" \ ENV_CONFIGFILE="ZOO_CFG Z00_AUTH KAFKA_CFG KAFKA_AUTH" \ POD_NAME="" ADD kafka_2.12-1.1.1.tgz . COPY zookeeper.properties server.properties kafka_server_jaas.conf zoo_server_jaas.conf /kafka_2.12-1.1.1/config/ RUN set -eux;\ groupadd -r kafka --gid=1000; \ useradd -r -g kafka --uid=1000 kafka; \ mkdir -p "$ZOO_DATA_DIR" "$ZOO_DATA_LOG_DIR" "$ZOO_LOG_DIR" ; \ chown -R kafka:kafka /kafka_2.12-1.1.1/ COPY --chown=root:root docker-entrypoint.sh pre-stop.sh / USER kafka:kafka CMD ["/bin/bash", "/docker-entrypoint.sh"] 这里声明了我们需要被用到的一些通用环境变量,还有镜像的一个构建方式 ...

January 9, 2022 · 5 min · Sxueck

使用BlackBox Exporter实现服务可用性检测

生产环境案例 场景分析 我们需要对三个集群进行监控,分别是 开发-测试集群 : blackbox-dev 预发布集群 : blackbox-pre 生产集群 : blackbox-pro Blackbox Exporter 配置 首先需要使用 BlackBox Exporter 实现一个探测 Module ,这个 Module 可以包含 HTTP、HTTPS、SSH 等方案,我们对目标进行监控的时候,可以直接使用 Prometheus 传入 Targets 以调用 Module 运行 Blackbox Exporter 时,需要用户提供探针的配置信息,这些配置信息可能是一些自定义的 HTTP 头信息,也可能是探测时需要的一些 TSL 配置,也可能是探针本身的验证行为,在 Blackbox Exporter 每一个探针配置称为一个 module,并且以 YAML 配置文件的形式提供给 Blackbox Exporter 如果我们想调用集群中的某个服务,必须先经过一个 API 网关,这个 API 网关兼备了访问控制的功能,每个用户需要在 Request 请求头中添加一个 Bearer Token —— 这取决于网关的具体配置,我们的探针也不例外,下面是 BlackBox Exporter 的配置示例 blackbox.yml modules: http_2xx: prober: http timeout: 5s http: preferred_ip_protocol: ip4 headers: Host: "probe.xxx.com" Cache-Control: no-cache bearer_token: '<token>' tcp_connect: prober: tcp timeout: 5s tcp: preferred_ip_protocol: ip4 icmp: prober: icmp timeout: 5s icmp: preferred_ip_protocol: ip4 http_2xx_home: prober: http timeout: 5s http: method: GET fail_if_body_not_matches_regexp: - "OK" fail_if_not_ssl: false preferred_ip_protocol: ip4 将服务启动后,我们访问 IP:9115 就能看见 WebUI 了 ...

January 9, 2022 · 4 min · Sxueck