Quantcast
Channel: BlogJava-聂永的博客
Viewing all 30 articles
Browse latest View live

Tsung笔记之IP直连支持篇

$
0
0

前言

前面说到设计一个小型的C/S类型远程终端套件以替换SSH,并且已经应用到线上。这个问题,其实不是Tsung自身的问题,是外部连接依赖问题。

Tsung在启动分布式压测时,主节点tsung_controller要连接的从机必须要填写主机名,主机名没有内网DNS服务器支持解析的情况下(我所经历互联网公司很少有提供支持的),只好费劲在/etc/hosts文件中填写主机名称和IP地址的映射关系,颇为麻烦,尤其是要添加一批新的压测从机或从机变动频率较大时。

那么如何解决这些问题呢,让tsung在复杂的机房内网环境下,完全基于IP进行直连,这将是本文所讨论的内容。

预备知识

完全限定域名

完全限定域名,缩写为FQDN (fully qualified domain name),赛门铁克给出的中文定义

一种用于指定计算机在域层次结构中确切位置的明确域名。
一台特定计算机或主机的完整 Internet 域名。FQDN 包括两部分:主机名和域名。例如 mycomputer.mydomain.com。
一种包含主机名和域名(包括顶级域)的 URL。例如,www.symantec.com 是完全限定域名。其中 www 是主机,symantec 是二级域,.com 是顶级域。FQDN 总是以主机名开始且以顶级域名结束,因此 www.sesa.symantec.com 也是一个 FQDN。

若机器主机名为内网域名形式,并且支持DNS解析,方便其它服务器可通过该主机名直接找到对应IP地址,能够 ping -c 3 机器域名 通,那么机器之间能够容易找到对方。

服务器hostname的命名,若不是域名形式,简短名称形式,比如“yk_mobile_dianxin_001”,一般内网的DNS服务器不支持解析,机器之间需要互相在/etc/hosts文件建立彼此IP地址映射关系才能够互相感知对方。

Erlang节点名称的规则

因为Tsung使用Erlang编写,Erlang关于节点启动名称规定,也是Tsung需要面对的问题。

Erlang节点名称一般需要遵循两种格式:

  1. 一般名称(也称之为短名称)形式,不包含“.”字符,比如 erl -name tsun_node
  2. 完全限定域名形式
    • 域名形式,比如erl -name tsun_node.youdomain.com
    • IP形式,比如erl -name 10.10.10.103

Tsung处理方式:

  • 若非特别指定,一般默认为短名称形式
  • 启动时可以通过-F参数指定使用完全限定域名形式

获得IP地址

主机名称无论是完全限定域名形式,还是简单的短名称形式,当别的主机需要通过主机名访问时,系统层面需要通过DNS系统解析成IP地址才能够进行网络连接。当内网DNS能够解析出来IP来,没有什么担心的;(短名称)解析不出来时,多半会通过写入到系统的 /etc/hosts 文件中,这样也能够解析成功。

一般机房内网环境,主机名称大都是短名称形式,若需分布式,每一个主机之间都要能够互相联通,最经济做法就是直接使用IP地址,可避免写入大量映射到 hosts 文件中,也会避免一些隐患。

主节点启动增加IP支持

默认情况下,Tsung Master主节点名称类似于tsung_controller@主机名

  • 节点名称前缀默认为:tsung_controller (除非在tsung启动时通过-i指定前缀)
  • 一般主机名都是字符串形式(hostname命令可设置主机名)
  • 可将主机名称设置为本机IP,但不符合人类认知惯性

既然Tsung主节点默认对IP节点名称支持不够,改造一下tsung/tsung.sh.in脚本。

Tsung启动时-F参数为指定使用完全限定域名(FQDN)形式,不支持携带参数。若要直接传递IP地址,类似于:

-F Your_IP

修改tsung.sh.in,可以传递IP地址,手动组装节点名称:

F) NAMETYPE="-name"
    SERVER_IP=$OPTARG
    if [ "$SERVER_IP" != "" ]; then
        CONTROLLER_EXTENDS="@$SERVER_IP"
    fi
    ;;

修改不复杂,更多细节请参考:https://github.com/weibomobile/tsung/blob/master/tsung.sh.in

启动Tsung时,指定本地IP:

tsung -F 10.10.10.10 -f tsung.xml start

tsung_controller目前节点名称已经变为:

-name tsung_controller@10.10.10.10

嗯,目标达成。

从节点主机增加IP配置

给出一个节点client50配置:

<client host="client50"  maxusers="100000" cpu="7" weight="4">
    <ip value="10.10.10.50"></ip>
    <ip value="10.10.10.51"></ip>
</client>

Tsung Master想访问client50,需要提前建立client50与IP地址的映射关系:

echo "10.10.10.50 client50" >> /etc/hosts

host属性默认情况下只能填写长短名称,无法填写IP地址,为了兼容已有规则,修改tsung-1.0.dtd文件为client元素新增一个hostip属性:

<!ATTLIST client
     cpu      NMTOKEN "1"
     type     (machine | batch)  "machine"
     host     NMTOKEN #IMPLIED
     hostip   CDATA ""
     batch    (torque | pbs | lsf | oar) #IMPLIED
     scan_intf NMTOKEN #IMPLIED
     maxusers NMTOKEN "800"
     use_controller_vm (true | false) "false"
     weight   NMTOKEN "1">

修改src/tsung_controller/ts_config.erl文件,增加处理逻辑,只有当主节点主机名为IP时才会取hostip作为主机名:

{ok, MasterHostname} = ts_utils:node_to_hostname(node()),
case {ts_utils:is_ip(MasterHostname), ts_utils:is_ip(Host), ts_utils:is_ip(HostIP)} of
   %% must be hostname and not ip:
    {false, true, _} ->
        io:format(standard_error,"ERROR: client config: 'host' attribute must be a hostname, "++ "not an IP ! (was ~p)~n",[Host]),
        exit({error, badhostname});
    {true, true, _} ->
        %% add a new client for each CPU
        lists:duplicate(CPU,#client{host     = Host,
                                    weight   = Weight/CPU,
                                    maxusers = MaxUsers});
    {true, _, true} ->
        %% add a new client for each CPU
        lists:duplicate(CPU,#client{host     = HostIP,
                                    weight   = Weight/CPU,
                                    maxusers = MaxUsers});
    {_, _, _} ->
        %% add a new client for each CPU
        lists:duplicate(CPU,#client{host     = Host,
                                    weight   = Weight/CPU,
                                    maxusers = MaxUsers})
end

嗯,现在可以这样配置从节点了,不用担心Tsung启动时是否附加-F参数了:

<client host="client50" hostip="10.10.10.50" maxusers="100000" cpu="7" weight="4">
    <ip value="10.10.10.50"></ip>
    <ip value="10.10.10.51"></ip>
</client>

其实,只要你确定只使用主节点主机名为IP地址,可以直接设置host属性值为IP值,可忽略hostip属性,但这以牺牲兼容性为代价的。

<client host="10.10.10.50" maxusers="100000" cpu="7" weight="4">
    <ip value="10.10.10.50"></ip>
    <ip value="10.10.10.51"></ip>
</client>

为了减少/etc/hosts大量映射写入,还是推荐全部IP形式,这种形式适合Tsung分布式集群所依赖服务器的快速租赁模型。

源码地址

针对Tsung最新代码增加的IP直连特性所有修改,已经放在github上:

https://github.com/weibomobile/tsung

并且已经递交pull requesthttps://github.com/processone/tsung/pull/189

比较有意思的是,有这样一条评论:

针对Tsung 1.6.0修改版

最近一次发行版是tsung 1.6.0,这个版本比较稳定,我实际压测所使用的就是在此版本上增加IP直连支持(如上所述),已经被单独放入到github上:

https://github.com/weibomobile/tsung-1.6.0

至于如何安装,git clone到本地,后面就是如何编译tsung的步骤了,不再累述。

小结

若要让IP直连特性生效,再次说明启用步骤一下:

  1. tsung.xml文件配置从机hostip属性,或host属性,填写正确IP
  2. tsung启动时,指定本机可用IP地址:tsung -F Your_Available_IP -f tsung.xml ... start

IP直连,再配合前面所写SSH替换方案,可以让Tsung分布式集群在复杂网络机房内网环境下适应性向前迈了一大步。

2016-08-06 更新此文,增加Tsung 1.6.0修改版描述



nieyong 2016-07-28 08:37 发表评论

Tsung笔记之监控数据收集篇

$
0
0

前言

压力测试和监控分不开,监控能够记录压测过程中状态,方便问题跟踪、定位。本篇我们将讨论对压测客户端tsung client的监控,以及对被压测服务器的资源占用监控等。同时,也涉及到Tsung运行时的实时诊断方式,这也是对Tsung一些运行时状态的主动监控。

压测客户端的监控

压测端(指的是tsung client)会收集每一个具体模拟终端用户(即ts_client模块)行为数据,发送给主节点(tsung_controller),供后面统计分析使用。

tsung_monitor_client

  1. ts_client模块调用ts_mon,而ts_mon又直接调用ts_mon_cache,有些绕,不直观(逻辑层面可忽略掉ts_mon)
  2. count为计数器,sum表示各项累加值,sample和sample_counter计算一次统计项的平均值&标准差
  3. tsung.dump文件一般不会创建&写入,除非你在tsung.xml文件中指定需要dump属性为true,压测数据量大时这个会影响性能
  4. match.log仅仅针对HTTP请求,默认不会写入,除非在HTTP压测指定

        <http url="/" method="GET" version="1.1"/> 
        <match do=’log’ when=’match’ name=’http_match_200ok’>200OK</match> 
    
  5. 从节点tsung client所记录日志、需要dump的请求-响应数据,都会交由tsung_controller处理

  6. ts_mon_cache,接收到数据统计内存计算,每500毫秒周期分发给后续模块,起到缓冲作用

  7. ts_stats_mon模块接收数据进行内存计算,结果写入由ts_mon触发

  8. ts_mon负责统计数据最每10秒定时写入各项统计数据到tsung.log文件,非实时,可避免磁盘IO开销过大问题

    • tsung/src/tsung_controller/tsung_controller.app.in 对应 {dumpstats_interval, 10000}
    • 可以在运行时修改
  9. tsung.log文件汇集了客户端连接、请求、完整会话、页面以及每一项的sum操作统计的完整记录,后续perl脚本报表分析基于此

  10. ts_mon模块处理tsung.log的最核心模块,全局唯一进程,标识为{global, ts_mon}

比如某次单机50万用户压测tsung.log日志片段:

# stats: dump at 1467620663
stats: users 7215 7215
stats: {freemem,"os_mon@yhg162"} 1 11212.35546875 0.0 11406.32421875 11212.35546875 11346.37109375 2
stats: {load,"tsung_controller@10.10.10.10"} 1 0.0 0.0 0.01171875 0.0 0.01171875 2                                                                                 17,1          Top
stats: {load,"os_mon@yhg162"} 1 2.3203125 0.0 3.96875 0.9609375 2.7558736313868613 411
stats: {recvpackets,"os_mon@yhg162"} 1 5874.0 0.0 604484 5874 319260.6024390246 410
stats: {sentpackets,"os_mon@yhg162"} 1 8134.0 0.0 593421 8134 293347.0707317074 410
stats: {cpu,"os_mon@yhg162"} 1 7.806645016237821 0.0 76.07377357701476 7.806645016237821 48.0447587419309 411
stats: {recvpackets,"tsung_controller@10.10.10.10"} 1 4164.0 0.0 45938 4164 24914.798543689314 412
stats: {sentpackets,"tsung_controller@10.10.10.10"} 1 4182.0 0.0 39888 4182 22939.191747572815 412
stats: {cpu,"tsung_controller@10.10.10.10"} 1 0.575191730576859 0.0 6.217097016796189 0.575191730576859 2.436491628709831 413
stats: session 137 2435928.551725737 197.4558174045777 2456320.3908691406 2435462.9838867188 2436053.875557659 499863
stats: users_count 0 500000
stats: finish_users_count 137 500000
stats: connect 0 0 0 1004.4912109375 0.278076171875 1.480528250488281 500000
stats: page 139 12.500138756182556 1.1243565417115737 2684.760009765625 0.43115234375 16.094989098940804 30499861
stats: request 139 12.500138756182556 1.1243565417115737 2684.760009765625 0.43115234375 16.094989098940804 30499861
stats: size_rcv 3336 3386044720
stats: size_sent 26132 6544251843
stats: connected -139 0
stats: error_connect_timeout 0 11

tsung.log日志文件可由tsung_stats.pl脚本提取、分析、整理成报表展示,其报表的一个摘要截图:

异常行为的收集

当模拟终端遇到网络连接超时、地址不可达等异常事件时,最终也会发给主节点的ts_mon模块,保存到tsung.log文件中。

这种异常记录,关键词前缀为 **error_**

  • 比如ts_client模块遇到连接超时会汇报error_connect_timeout错误
  • 系统的可用端口不够用时(创建与压测服务器连接数超出可用段限制)上报error_connect_eaddrinuse错误

Errors报表好比客户端出现问题晴雨表,再加上tsung输出log日志文件,很清楚的呈现压测过程中出现的问题汇集,方便问题快速定位。

被压测服务器的监控

当前tsung提供了3种方式进行监控目标服务器资源占用情况:

  • erlang
  • snmp
  • Munin

大致交互功能,粗略使用一张图表示:

tsung_server_monito

  • tsung_controller主节点会被强制启用监控
  • SNMP方式,客户端作为代理主动注册并连接开放SNMP的服务器,SNMP安装针对新手来说比较复杂
  • Munin采用C/S模式,自身要作为客户端连接被压测服务器上能够安装Munin Server
  • erlang方式,本身代理形式监控服务器资源占用,满足条件很简单:
    • 需要能够自动登录连接
    • 并且安装有Erlang运行时环境,tsung_controller方便启动监控节点
    • 采用远程加载方式业务代码,省去被监控端部署的麻烦
    • 现实情况下,我一般采用一个脚本搞定自动部署监控部署客户端,自动打包可移植的Erlang,简单绿色,部署方便
  • 提供监控采样数据包括 CPU/Memory/Load/Socket Sent&Recv
  • 所有监控数据都会被发送给ts_mon模块,并定时写入到tsung.log文件中

看一个最终报表部分呈现吧:

tsung对服务器监控采样手机数据不是很丰富,因为它面向的更为通用的监控需求。

更深层次、更细粒度资源监控,就需要自行采集、自行分析了,一般在商业产品在这方面会有更明确需求。

日志收集

和前面讲到的终端行为数据采集和服务器端资源监控行为类似,tsung运行过程中所产生日志被存储到主节点。

tsung使用error_logger记录日志,主节点tsung_controller启动之后,会并发启动tsung client从节点,换句话来说tsung client从节点是由主节点tsung_controller创建,这个特性决定了tsung client从节点使用error_logger记录的日志都会被重定向到主节点tsung_controller所在服务器上,这个是由Erlang自身独特机制决定。

因此,你在主节点log目录下能够看到具体的日志输出文件,也就水到渠成了。因为Erlang天生分布式基因,从节点error_logger日志输出透明重定向到主节点,不费吹灰之力。这在其他语言看来,确实完全不可能轻易实现的。

基于error_logger包装日志记录,需要一个步骤:

  1. 设置输出到文件系统中 error_logger:tty(false)
  2. 设定输出的文件目录 error_logger:logfile({open, LogFile})
  3. 包装日志输出接口 ?DEBUG/?DEBUGF/?LOG/?LOGF/
  4. 最终调用包装的error_logger接口
debug(From, Message, Args, Level) ->
    Debug_level = ?config(debug_level),
    if
        Level =< Debug_level ->
            error_logger:info_msg("~20s:(~p:~p) "++ Message,
                                  [From, Level, self()] ++ Args);
        true ->
            nodebug
    end.

和大部分日志框架设定的日志等级一致,emergency > critical > error > warning > notice (default) > info > debug,从左到右,依次递减。

需要注意事项,error_logger语义为记录错误日志,只适用于真正的异常情况,并不期望过多的消息量的处理。

若当一般业务调试类型日志量过多时,不但耗费了大量内存,网络/磁盘写入速度跟不上生产速度时,会导致进程堵塞,严重会拖累整个应用僵死,因此需要在tsung.xml文件中设置至少info级别,至少默认的notice就很合适。

Tsung运行时诊断/监控

Tsung在运行时,我们可以remote shell方式连接登录进去。

为了连接方便,我写了一个脚本 connect_tsung.sh,只需要传入tsung节点名称即可:

# !/bin/bash
## 访问远程Tsung节点 sh connect\_tsung.sh tsung\_controller@10.10.10.10

HOST=`ifconfig | grep "inet " | grep -v "127.0.0.1" | head -1 | awk '{print $2}' | cut -d / -f 1`
if [ -z $HOST ]; then
    HOST = "127.0.0.1"
fi
erl -name tmp\_$RANDOM@$HOST -setcookie tsung -remsh $1

需要安装有Erlang运行时环境支持

当然,要向运行脚本,你得知道Tsung所有节点名称。

如何获得tsung节点名称

其实有两种方式获得Tsung节点名称:

  • 直接连接tsung_controller节点获得
    • 若是IP形式,sh connect_tsung.sh tsung_controller@10.10.10.10
    • 若是hostname形式,可以这样:sh connect_tsung.sh tsung_controller@tsung_master_hostname
    • 成功进入之后,输入 nodes(). 可以获得完整tsung client节点列表
  • 启动tsung时生成日志所在目录,可以看到类似日志文件:
    • tsung client端产生日志单独存放,格式为节点名称.log
    • eg: tsung15@10.10.10.113.log,那么节点名称为tsung15@10.10.10.113
    • 可以直接连接:sh connect_tsung.sh tsung15@10.10.10.ll3

如何诊断/监控Tsung运行时

其实,这里仅仅针对使用Erlang并且对Tsung感兴趣的同学,你都能够进来了,那么如何进行查看、调试运行时tsung系统运行情况,那么就很简单了。推荐使用 recon 库,包括内存占用,函数运行堆栈,CPU资源分配等,一目了然。

若问,tsung启动时如何添加recon依赖,也不复杂:

  1. 每一个运行tsung的服务器拷贝已经编译完成的recon项目到指定目录
  2. tsung_controller主节点启动时,指定recon依赖库位置

    tsung -X /Your_Save_Path/recon/ebin/ ...

说一个用例,修改监控数据每10秒写入tsung.log文件时间间隔值,10秒修改为5秒:

application:set_env(tsung_controller, dumpstats_interval, 5000).

执行之后,会立刻生效。

小结

总结了Tsung主从监控,以及服务器端监控部分,以及运行时监控等。提供的被压测服务器监控功能很粗,仅收集CPU、内存、负载、接收数据等类型峰值,具有一般参考意义。但基于Tsung构建的、或类似商业产品,一般会有提供专门数据收集服务器,但对于开源的应用而言,需要兼顾通用需求,也是能够理解的。



nieyong 2016-07-29 08:49 发表评论

Tsung笔记之插件编写篇

$
0
0

前言

Tsung对具体协议、通道的支持,一般以插件形式提供接口,接口不是很复杂,插件也很容易编写,支持协议多,也就不足为怪了。

下面首先梳理一下当前Tsung 1.6.0所有内置插件,然后为一个名称为Qmsg的私有二进制协议编写插件, 运行Qmsg服务器端程序,执行压力测试,最后查看测试报告。

已支持插件梳理

Tsung 1.6.0支持的协议很多,简单梳理一下:

Tsung Controller  Support Plugins V2-1

  • 压测的协议首先需要支持xml形式配置,配置内容需要 tsung_config_protocolname 模块解析
    • 存放在tsung_controller目录下
  • 其次是tsung client端也要插件 ts_protocolname 模块支持数据操作
    • 存放在tsung目录下
  • 同时在tsung项目examples目录下也给出了已支持协议配置简单示范xml文件

已经支持协议简单说明:

  1. amqp,Advanced Message Queuing Protocol缩写,只要支持高级消息队列协议的应用,都可以用来做压测,比如RabbitMQ,ActiveMQ等
  2. http,基本协议,构建于HTTP协议之上的,还有类似于BOSH,WebDav等上层业务协议
  3. jabber,也称之为XMPP,支持的相当丰富,除了TCP/SSl,还可以通过Websocekt进行传递
  4. raw,针对原始类型消息,不做编解码处理,直接在TCP / UDP / SSL等传输层中传递,这个对部分私有协议,比较友好,不用写单独的编解码处理,直接透传好了
  5. shell,针对LInux/Unix终端命令调用进行压测,这种场景比较小众
  6. fs,filesystem缩写,针对文件系统的读写性能进行压测
  7. job,针对任务调度程序进行的压测,比如PBS/torqueLSF、OAR等

Tsung插件工作机制

粗一点来看Tsung插件的工作流程(点击可以看大图):

tsung_qmsg_flo

放大一些(引用 hncscwc 博客图片,相当赞!):

为什么要编写插件

Tsung针对通用协议有支持,若是私有或不那么通用的协议,就不会有专门的插件支持了,那么可选的有两条路子:

  • 使用raw模式发送原始消息,需要自行组装
  • 自己编写插件,灵活处理编解码

既然谈到了插件,我们也编写一个插件也体验一下编写插件的过程。

Qmsg协议定义

假设一个虚拟场景,打造一个新的协议Qmsg,二进制格式组成:

qmsg_protoco

这种随意假象出来的格式,不妨称作为qmsg(Q可爱形式的message)协议,仅作为Demo演示而存在。简单场景:

  • 用户发言,包含用户id和发言内容
    • User ID,32位自然数类型
    • 发言为文字内容,字符串形式,长度不固定
    • 组装后的请求体为二进制协议格式
    • PocketLen:**##UserId + UserComment##**
  • 服务器端返回用户ID和一个幸运数字(32位表示)
    • PocketLen:**##UserId + RandomCode##**

为了卡哇伊一些,多了一些点缀的“**####**”符号。

编写一个完整插件

这里基于Tsung 1.6.0版本构建一个Qmsg插件,假定你懂一些Erlang代码,以及熟悉Tsung一些基本概念。

0. 创建一个项目

要创建Tsung的一个Qmsg插件项目,虽没有固定规范,但按照已有格式组织好代码层级也是有必要的。

├── include
│   └── ts_qmsg.hrl
├── src
│   ├── tsung
│   │   └── ts_qmsg.erl
│   └── tsung_controller
│       └── ts_config_qmsg.erl
└── tsung-1.0.dtd

1. 创建配置文件

Tsung的压测以xml文件驱动,因此需要界定一个Qmsg插件形式的完整会话的XML呈现,比如:

<session probability="100" name="qmsg-demo" type="ts_qmsg">
    <request>
      <qmsg uid="1001">Hello Tsung Plugin</qmsg>
    </request>

    <request>
      <qmsg uid="1002">This is a Tsung Plugin</qmsg>
    </request>
</session>
  • ts_qmsg,会话类型所依赖协议模拟客户端实现
  • <qmsg uid="Number">Text</qmsg> 定义了qmsg会话可配置形式,内嵌在request元素内
  • uid为属性

此时,你若直接在xml文件中编辑,会遇到校验错误。

2. 更新DTD文件

Tsung的xml文件依赖tsung-1.0.dtd文件进行校验配置是否有误,需要做对DTD文件做修改,以支持所添加新的协议。

tsung-1.0.dtd项目中,最小支持:

  1. session元素type属性中添加上 ts_qmsg
  2. request元素处添加 qmsg : <!ELEMENT request ( match*, dyn_variable*, ( http | jabber | raw | pgsql | ldap | mysql |fs | shell | job | websocket | amqp | mqtt | qmsg) )>
  3. 添加qmsg元素定义:
<!ELEMENT qmsg (#PCDATA) >
<!ATTLIST qmsg
    uid         CDATA   "0"
    ack         (local | no_ack | parse) #REQUIRED
    >

完整内容,可参考tsung_plugin_demo/tsung-1.0.dtd文件。

3. 头文件 include/ts_qmsg.hrl

头文件include/ts_qmsg.hrl定义数据保存的结构(也称之为记录/record):

-record(qmsg_request, {
          uid,
          data
         }).

-record(qmsg_dyndata, {
          none
         }
       ).
  1. qmsg_request: 存储从xml文件解析的qmsg请求数据,用于生成压力请求
  2. qmsg_dyndata: 存储动态参数(当前暂未使用到)

4. XML文件解析

ts_config_qmsg.erl文件,用于解析和协议Qmsg关联的配置:
- 只需要实现parse_config/2唯一方法
- 解析xml文件中所配置Qmsg协议请求相关配置
- 被ts_config:parse/1在遇到Qmsg协议配置时调用

备注:

  1. 若要支持动态替换,需要的字段以字符串形式读和存储

5. ts_qmsg.erl

ts_qmsg.erl模块主要提供Qmsg协议的编解码的完整动作, 以及当前协议界定下的用户会话属性设定。

首先需要实现接口ts_plugin规范定义的所有需要函数,定义了参数值和返回值。

-behavior(ts_plugin).

...

-export([add_dynparams/4,
         get_message/2,
         session_defaults/0,
         subst/2,
         parse/2,
         parse_bidi/2,
         dump/2,
         parse_config/2,
         decode_buffer/2,
         new_session/0]).

相对来说,核心为协议的编解码功能:

  • get_message/2,构造请求数据,编码成二进制,上层ts_client模块通过Socket连接发送给目标服务器
  • parse/2,(当对响应作出校验时)从原始Socket上返回的数据进行解码,取出协议定义业务内容

这部分代码可以参考 tsung_plugin_demo/src/tsung/ts_client.erl 文件。

6. 如何编译

虽然理论上可以单独编,生成的beam文件直接拷贝到已经安装的tsung对应目录下面,但实际上插件编写过程中要依赖多个tsung的hrl文件,这造成了依赖路径问题。采用直接和tsung打包一起部署,实际操作上有些麻烦,

为了节省体力,使用一个shell脚本 - build_plugin.sh,方便快速编译、部署:

# !/bin/bash

cp tsung-1.0.dtd $1/
cp include/ts_qmsg.hrl $1/include/
cp src/tsung_controller/ts_config_qmsg.erl $1/src/tsung_controller/
cp src/tsung/ts_qmsg.erl $1/src/tsung/

cd $1/
make uninstall
./configure --prefix=/usr/local
make install

这里指定安装Tsung的指定目录为/usr/local,可以根据需要修改

需要提前准备好tsung-1.6.0目录:

wget http://tsung.erlang-projects.org/dist/tsung-1.6.0.tar.gz
tar xf tsung-1.6.0.tar.gz

在编译Qmsg插件脚本时, 指定一下tsung-1.6.0解压后的路径即可:

sh build_plugin.sh /your_path/tsung-1.6.0

后面嘛,就等着自动编译和安装呗。

启动Qmsg协议的压测

1. 首先启动Qmsg服务器端程序

既然有压测端,就需要一个Qmsg协议处理的后端程序qmsg_server.erl,用于接收客户端请求,获得用户ID值之后,生成一个随机数字,组装成二进制协议,然后发给客户端,这就是全部功能。

这个程序,简单一个文件,在 tsung_plugin_demo目录下面,编译运行, 默认监听5678端口:

erlc qmsg_server.erl && erl -s qmsg_server start

另外,还提供了一个手动调用接口,方便在Erlang Shell端调试:

%% 下面为
qmsg_server:sendmsg(1001, "这里是用户发言").

启动之后,监听地址 *: 5678

源码见:tsung_plugin_demo/qmsg_server.erl

2. 编写Qmsg压测XML配置文件

因为是演示示范,一台Linxu主机上就可以进行了:

  • 连接本机的 127.0.0.1:5678
  • 最多产生10个用户,每秒产生1个,压力负载设置的很低
  • 两个不同类型会话,比重10% + 90% = 100%
  • qmsg-subst-example会话使用了用户ID个和用户发言内容自动生成机制
<tsung loglevel="debug" dumptraffic="false" version="1.0">
  <clients>
    <client host="localhost" use_controller_vm="true"/>
  </clients>

  <servers>
    <server host="127.0.0.1" port="5678" type="tcp"/>
  </servers>

  <load>
    <arrivalphase phase="1" duration="1" unit="minute">
      <users maxnumber="10" interarrival="1" unit="second"/>
    </arrivalphase>
  </load>

  <sessions>
    <session probability="10" name="qmsg-example" type="ts_qmsg">
      <request>
        <qmsg uid="1001" ack="parse">Hello Tsung Plugin Qmsg!</qmsg>
      </request>
    </session>
    <session probability="90" name="qmsg-subst-example" type="ts_qmsg">
      <setdynvars sourcetype="random_number" start="3" end="32">
        <var name="random_uid"/>
      </setdynvars>
      <setdynvars sourcetype="random_string" length="13">
        <var name="random_txt"/>
      </setdynvars>
      <request subst="true">
        <qmsg uid="%%_random_uid%%" ack="parse">Haha : %%_random_txt%%</qmsg>
      </request>
      <thinktime value="6"/>
      <request subst="true">
        <qmsg uid="%%_random_uid%%" ack="parse">This is a Tsung Plugin</qmsg>
      </request>
    </session>
  </sessions>
</tsung>

这部分内容,请参考 tsung_plugin_demo/tsung_qmsg.xml 文件。

3. 执行压力测试

当Qmsg的压力测试配置文件写好之后,可以开始执行压力测试了:

tsung -f tsung_qmsg.xml start

其输出:

tarting Tsung
Log directory is: /root/.tsung/log/20160621-1334
[os_mon] memory supervisor port (memsup): Erlang has closed
[os_mon] cpu supervisor port (cpu_sup): Erlang has closed

其中, 其日志为:/root/.tsung/log/20160621-1334

4. 查看压测报告

进入其生成压测日志目录,然后生成报表,查看压测结果哈:

cd /root/.tsung/log/20160621-1334

/usr/local/lib/tsung/bin/tsung_stats.pl

echo "open your browser (URL: http://IP:8000/report.html) and vist the report now :))"
/usr/bin/python -m SimpleHTTPServer

嗯,打开你的浏览器,输出所在服务器的IP地址,就可以看到压测结果了。

小结

以上代码已经放入github仓库:https://github.com/weibomobile/tsung_plugin_demo

实际业务的私有协议内容要比上面Demo出来的Qmsg复杂的多,但其私有协议插件编写,如上面所述几个步骤,按照规范编写,单机测试,然后延伸到分布式集群,完整流程都是一致的。

嗯,搞定了插件,就可以对系统愉快地进行压测了 :))



nieyong 2016-07-30 19:37 发表评论

Tsung笔记之100万用户压测执行步骤篇

$
0
0

前言

总是说细节、理论,会让人不胜其烦。我们使用Tsung来一次100万用户压测的吧,或许能够引起好多人的兴趣 :))

下面,我根据在公司分享的PPT《分布式百万用户压测你的业务》,贴出其中的关键部分,说明进行一次100W(即1M)用户压测的执行步骤。

如何做分布式百万用户的压测 ?

假定面向小白用户,因此才有了下面可执行的10个步骤用于开展分布式百万用户。

看着步骤很多,一旦熟悉并掌握之后,中间可以省却若干。

1. 阅读Tsung文档

建议大家在使用Tsung之前,花费一点时间阅读完整个用户手册,虽然是英文的,阅读起来也不复杂。读完之后,我们也就知道如何做测试了,遇到的大部分问题,也能够在里面找到答案。

2 确定压测目标

  • 要对线上系统压测100万用户,为了尽可能降低线上服务器负载压力,这里设置每秒产生500个用户,将在60分钟内产生完毕
  • 要压测的服务器所填写网络访问地址可以根据需要填写多个

3. 计算所需要从机数量






  • Tsung为主从模型,我们启动了主节点之后,主节点会按需启动从节点
  • 设定所用服务器可用内存大于3G,并且都只有一个IP地址
  • 一台从机可用模拟6万用户,需要17台从机
  • 若资源充足,可以少用几台服务器,配置多个IP地址
  • 找到所需要的压测用服务器,在资源层面满足测试测试集群需要,这个是关键

4. 部署Tsung

因为Tsung依赖于Erlang,因此需要首先安装:

wget https://packages.erlang-solutions.com/erlang-solutions-1.0-1.noarch.rpm
rpm -Uvh erlang-solutions-1.0-1.noarch.rpm
sudo yum install erlang

然后再是安装Tsung,建议直接使用Tsung 1.6.0修改版,主要提供IP只连支持(具体细节,可参考这里 http://www.blogjava.net/yongboy/archive/2016/07/28/431354.html ):

git clone https://github.com/weibomobile/tsung-1.6.0.git
./configure --prefix=/usr/local
make install

5. 下载SSH替代者-tsung—rsh

为什么要替换掉SSH,主要原因:

  • SSH在一般网络机房环境内服务器之间被禁止连接通信,这会导致主节点无法启动从节点,无法建立分布式压测集群
  • 就算是SSH没被禁用,主从之间需要设置免秘钥SSH登录方式,十分麻烦

可进一步参考:Tsung笔记之分布式增强跳出SSH羁绊篇

6. 编写压测内容



要把业务定义的所有会话内容完整的整理映射成Tsung的会话内容,因为用户行为很复杂,也需要我们想法设法去模拟。

其实,演示所使用的是私有协议,可以参考 Tsung笔记之插件编写篇

当完成压测会话内容之后,users_100w.xml文件已经填写完毕,我们可以开始压测了。

7. 运行Tsung

  • -F 10.10.10.10 主节点IP地址,IP直连特性
  • -rsh rsh_client.sh 远程终端,SSH通道被替换
  • -s 压测端启用erlang smp特性,按需使用所有CPU核心

我们启动了从节点,然后从节点被启动,开始执行具体压测任务了。

8. 压测过程中,我们该做什么

紧密关注服务器服务状态、资源占用等情况就对了,最好还要作为一个终端用户参与到产品体验中去。

9. 压测结束,生成Tsung报表

Tsung压测结束之后,不会主动生成压测结果报表的,需要借助于 tsung_stats.pl perl脚本生成,要查阅可借助python生成临Web站点,浏览器打开即可。

10. 回顾和总结

小结

其实,一旦熟悉并掌握Tsung之后,步骤1-6都可以节省了,循环执行步骤7-10。

你若以为仅仅只是谈论Tsung如何做1M用户压测,那就错了,只要机器资源够,这个目标就很容易实现。我们更应该关注,我们压测的目的是什么,我们应该关注什么,这个应该形成一个完整可循环过程,驱动着系统架构健康先前发展。



nieyong 2016-08-08 21:31 发表评论

Tsung笔记之IP地址和端口限制突破篇

$
0
0

前言

Tsung笔记之压测端资源限制篇中说到单一IP地址的服务器最多能够向外发送64K个连接,这个已算是极限了。

但现在我还想继续深入一下,如何突破这个限制呢 ?

如何突破限制

这部分就是要从多个方面去讨论如何如何突破限制单个IP的限制。

0. Tsung支持TCP情况

在Tsung 1.6.0 中支持的TCP属性有限,全部特性如下:

protocol_options(#proto_opts{tcp_rcv_size = Rcv, tcp_snd_size = Snd,
                             tcp_reuseaddr = Reuseaddr}) ->
    [binary,
     {active, once},
     {reuseaddr, Reuseaddr},
     {recbuf, Rcv},
     {sndbuf, Snd},
     {keepalive, true} %% FIXME: should be an option
    ].

比如可以配置地址重用:

<option name="tcp_reuseaddr" value="true" />

1. 增加IP地址

这是最为现实、最为方便的办法,向运维的同事多申请若干个IP地址就好。在不考虑其它因素前提下,一个IP地址可以对外建立64K个连接,多个IP就是N * 64K了。这个在Tsung中支持的很好。

<client host="client_99" maxusers="120000" weight="2" cpu="8">
    <ip value="10.10.10.99"></ip>
    <ip value="10.10.10.11"></ip>
</client>

2. 考虑Linux内核新增SO_REUSEPORT端口重用特性

以被压测的一个TCP服务器为例,继续拿网络四元组说事。

{SrcIp, SrcPort, TargetIp, TargetPort}
  • 线上大部分服务器所使用的系统为CentOS 6系列,所使用系统内核低于3.9
    • {SrcIp, SrcPort} 确定了本地建立一个连接的唯一性,本地地址的唯一性
    • {TargetIp, TargetPort}的无法确定唯一,仅仅标识了目的地址
  • Linux Kernel 3.9 支持 SO_REUSEPORT 端口重用特性 - 网络四元组中,任何一个元素值的变化都会成为一个全新的连接
    • 真正让网络四元组一起组成了一个网络连接的唯一性
    • 理论上可以对外建立的连接数依赖于四个元素可变数值
    • Totalconnections = NSrcIp * NSrcPort * NTargetIp * NTargetPort

线上有部分服务器安装有CentOS 7,其内核为3.10.0,很自然支持端口重用特性。

针对只有一个IP地址的压测端服务器而言,端口范围也就确定了,只能从目标服务器连接地址上去考虑。有两种方式:

  1. 目标服务器增加多个可用IP地址,服务程序绑定指定端口即可
    • N个IP地址,可用存在 64K * N
  2. 服务程序绑定多个Port,这个针对程序而言难度不大
    • 针对单个IP,监听了M个端口
    • 可用建立 64K * M 个连接
  3. 可用这样梳理 , Total1 ip connections = 64K * N * M

啰嗦了半天,但目前Tsung还没有打算要提供支持呢,怎么办,自己动手丰衣足食吧:

https://github.com/weibomobile/tsung/commit/f81288539f8e6b6546cb9e239c36f05fc3e1b874

3. 透明代理模式支持

Linux Kernel 2.6.28提供IP_TRANSPARENT特性,支持可以绑定不是本机的IP地址。这种IP地址的绑定不需要显示的配置在物理网卡、虚拟网卡上面,避免了很多手动操作的麻烦。但是需要主动指定这种配置,比如下面的C语言版本代码

int opt =1;
setsockopt(server_socket, SOL_IP, IP_TRANSPARENT, &opt, sizeof(opt));

目前在最新即将打包的1.6.1版本中提供了对TCP的支持,也需要翻译成对应的选项,以便在建立网络连接时使用:

说明一下:
- IP_TRANSPARENT没有对应专门的宏变量,其具体值为19
- SOL_IP定义宏对应值:0
- 添加Socket选项通用格式为:{raw, Protocol, OptionNum, ValueSpec}

那么如何让透明代理模式工作呢?

3.1 启用IP_TRANSPARENT特性
<options>
    ...
    <option name="ip_transparent" value="true" />
    ...
<options>
3.2 配置可用的额外IP地址

那么这些额外的IP地址如何设置呢?

  • 可以为client元素手动添加多个可用的IP地址 xml <client host="tsung_client1" maxusers="500000" weight="1"> <ip value="10.10.10.117"/> <ip value="10.10.10.118"/> ...... <ip value="10.10.10.127"/> </client>
  • 可以使用新增的iprange特性 xml <client host="tsung_client1" maxusers="500000" weight="1"> <ip value="10.10.10.117"/> <iprange version="v4" value="10.10.10-30.1-254"/> </client>

但是需要确保:

  1. 这些IP地址目前都没有被已有服务器在使用
  2. 并且可以被正常绑定到物理/虚拟网卡上面
  3. 完全可用
3.3 配置路由规则支持

假设我们的tsung_client1这台压测端服务器,绑定所有额外IP地址到物理网卡eth1上,那么需要手动添加路由规则:

ip rule add iif eth1 tab 100
ip route add local 0.0.0.0/0 dev lo tab 100

这个支持压测端绑定同一网段的可用IP地址,比如压测端IP为172.16.247.130,172.16.247.201暂时空闲的话,那我们就可以使用172.16.89.201这个IP地址用于压测。此时不要求被压测的服务器配置什么。

3.4 进阶,我们使用一个新的网段专用于测试

比如 10.10.10.0 这个段的IP机房暂时没有使用,那我们专用于压测使用,这样一台服务器就有了250多个可用的IP地址了。

压测端前面已经配置好了,现在需要为被压测的服务器添加路由规则,这样在响应数据包的时候能够路由到压测端:

route add -net 10.10.10.0 netmask 255.255.255.0 gw 172.16.247.130

设置完成,可以通过route -n命令查看当前所有路由规则:

在不需要时,可以删除掉:

route del -net 10.10.10.0 netmask 255.255.255.0

小结

梳理了以上所能够想到的方式,以尽可能突破单机的限制,核心还是尽可能找到足够多可用的IP地址,利用Linux内核特性支持,程序层面绑定尽可能多的IP地址,建立更多的对外连接。当然以上没有考虑类似于CPU、内存等资源限制,实际操作时,还是需要考虑这些资源的限制的。



nieyong 2016-08-16 21:17 发表评论

HTTP API设计笔记

$
0
0

前言

最近一段时间,要为一个手机终端APP程序从零开始设计一整套HTTP API,因为面向的用户很固定,一个新的移动端APP。目前还是项目初期,自然要求一切快速、从简,实用性为主。

下面将逐一论述我们是如何设计HTTP API,虽然相对大部分人而言,没有什么新意,但对我来说很新鲜的。避免忘却,趁着空闲尽快记录下来。

技术堆栈的选择

PHP嘛?团队内也没几个人熟悉。

Java?好几年没有碰过了,那么复杂的解决方案,再加上团队内也没什么人会 ……

团队使用过Lua,基于OpenResty构建过TCP、HTTP网关等,对Lua + Nginx组合非常熟悉,能够快速的应用在线上环境。再说Lua语法小巧、简单,一个新手半天就可以基本熟悉,马上开工。

看来,Nginx + Lua是目前最为适合我们的了。

HTTP API,需要充分利用HTTP具体操作语义,来应对具体的业务操作方法。基于此,没有闭门造车,我们选择了 http://lor.sumory.com/ 这么一个小巧的框架,用于辅助HTTP API的开发开发。

嗯,OpenResty + Lua + Lor,就构成了我们简单技术堆栈。

HTTP API简要设计

HTTP API路径和语义

每一具体业务逻辑,直接在URL Path中体现出来。我们要的是简单快速,数据结构之间的连接关系,尽可能的去淡化。eg:

/resource/video/ID

比如用户反馈这一模块,将使用下面比较固定的路径:

/user/feedback
  • GET,以用户维度查询反馈的历史列表,可分页
    • curl -X GET http://localhost/user/feedback?page=1
  • POST,提交一个反馈
    • curl -X POST http://localhost/user/feedback -d "content=hello"
  • DELETE,删除一个或多个反馈,参数附加在URL路径中。
    • curl -X DELETE http://localhost/user/feedback?id=1001
  • PUT,更新评论内容
    • curl -X PUT http://localhost/user/feedback/1234 -d "content=hello2"

用户属性很多,用户昵称只是其中一个部分,因此更新昵称这一行为,HTTP的 PATCH 方法可更精准的描述部分数据更新的业务需求:

/user/nickname
  • PATCH,更新用户昵称,昵称是用户属性之一,可以使用更轻量级的 PATCH 语义
    • curl -X PATCH http://localhost/user/nickname -d "nickname=hello2"

嗯,同一类的资源URL虽然固定了,但HTTP Method呈现了不同的业务逻辑需求。

HTTP API的访问授权

实际业务HTTP API的访问是需要授权的。

传统的Access Token解决方案,有session回话机制,一般需要结合Web浏览器,需要写入到Cookie中,或生产一个JSessionID用于标识等。这针对单纯面向移动终端的HTTP API后端来讲,并没有义务去做这一的兼容,略显冗余。

另外就是 OAUTH 认证了,有整套的认证方案并已工业化,很是成熟了,但对我们而言还是太重,不太适合轻量级的HTTP API,不太可能花费太多的精力去做它的运维工作。

最终选择了轻量级的 Json Web Token,非常紧凑,开箱即用。

最佳做法是把JWT Token放在HTTP请求头部中,不至于和其它参数混淆:

curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiI2NyIsInV0eXBlIjoxfQ.LjkZYriurTqIpHSMvojNZZ60J0SZHpqN3TNQeEMSPO8" -X GET http://localhost/user/info

下面是一副浏览器段的一般认证流程,这与HTTP API认证大体一致:

JWT的Lua实现,推荐: https://github.com/SkyLothar/lua-resty-jwt.git,简单够用。

JWT和Lor的结合

jwt需要和业务进行绑定,结合 lor 这个API开发框架提供的中间件机制,可在业务处理之前,在合适位置进行权限拦截。

  • 用户需要请求进行授权接口,比如登陆等
  • 服务器端会把用户标识符,比如用户id等,存入JWT的payload负荷中,然后生成Token字符串,发给客户端
  • 客户端收到JWT生成的Token字符串,在后续的请求中需要附加在HTTP请求的Header中
  • 完成认证过程

不同于OAUTH,JWT协议的自包含特性,决定了后端可以将很多属性信息存放在payload负荷中,其token生成之后后端可以不用存储;下次客户端发送请求时会发送给服务器端,后端获取之后,直接验证即可,验证通过,可以直接读取原先保存其中的所有属性。

下面梳理一下Jwt认证和Lor的结合。

  • 全局拦截,针对所有PATH,所有HTTP Method,这里处理JWT认证,若认证成功,会直接把用户id注入到当前业务处理上下文中,后面的业务可以直接读取当前用户的id值
app:use(function(req, res, next)
    local token = ngx.req.get_headers()["Authorization"]
    -- 校验失败,err为错误代码,比如 400
    local payload, err = verify_jwt(token)
    if err then
        res:status(err):send("bad access token reqeust")
        return
    end

    -- 注入进当前上下文中,避免每次从token中获取
    req.params.uid = payload.uid

    next()
end)
  • 针对具体路径进行设定权限拦截,较粗粒度;比如 /user 只允许已登陆授权用户访问
app:use("/user", function(req, res, next)
    if not req.params.uid then
        -- 注意,这里没有调用next()方法,请求到这里就截止了,不在匹配后面的路由
        res:status(403):send("not allowed reqeust")
    else
        next() -- 满足以上条件,那么继续匹配下一个路由
    end
end)
  • 一种是较细粒度,具体到每一个API接口,因为虽然URL一致,但不同的HTTP Method有时请求权限还是有区别的
local function check_token(req, res, next)
    if not req.params.uid then
        res:status(403):send("not allowed reqeust")
    else
        next()
    end
end

local function check_master(req, res, next)
    if not req.params.uid ~= master_uid then
        res:status(403):send("not allowed reqeust")
    else
        next()
    end
end

local lor = require("lor.index")
local app = lor()

-- 声明一个group router
local user_router = lor:Router()

-- 假设查看是不需要用户权限的
user_router:get("/feedback", function(req, res, next)
end)

user_router:put("/feedback", check_token, function(req, res, next)
end)

user_router:post("/feedback", check_token, function(req, res, next)
end)

-- 只有管理员才有权限删除
user_router:delete("/feedback", check_master, function(req, res, next)
end)

-- 以middleware的形式将该group router加载进来
app:use("/user", user_router())

......

app:run()

为什么没有选择GraphQL API ?

我们在上一个项目中对外提供了GraphQL API,其(在测试环境下)自身提供文档输出自托管机制,再结合方便的调试客户端,确实让后端开发和前端APP开发大大降低了频繁交流的频率,节省了若干流量,但前期还是需要较多的培训投入。

但在新项目中,一度想提供GraphQL API,遇到的问题如下:

  • 全新的项目数据结构属性变动太频繁
  • 普遍求快,业务模型快速开发、调试
  • 大家普遍对GraphQL API有些抵触,使用JSON输出格式的HTTP API是约定俗成的习惯选择

毫无疑问,以最低成本快速构建较为完整的APP功能,HTTP API + JSON格式是最为舒服的选择。

虽然有些担心服务器端的输出,很多时候还是会浪费掉一些流量,客户端并不能够有效的利用返回数据的所有字段属性。但和进度以及人们已经习惯的HTTP API调用方式相比,又微乎其微了。

小结

当前这一套HTTP API技术堆栈运行的还不错,希望能给有同样需要的同学提供一点点的参考价值 :))

当然没有一成不变的架构模型,随着业务的逐渐发展,后面相信会有很多的变动。但这是以后的事情了,谁知道呢,后面有空再次记录吧~



nieyong 2018-01-02 20:53 发表评论

Apisix 1.5 升级到 2.2 踩坑备忘

$
0
0

零、前言

线上运行的 APISIX 为 1.5 版本,而社区已经发布了 Apisix 2.2,是时候需要升级到最新版了,能够享受最版本带来的大量的BugFix,性能增强,以及新增特性的支持等~

从Apisix 1.5升级到Apisix 2.2过程中,不是一帆风顺的,中间踩了不少坑,所谓前车之鉴后事之师,这里给大家简单梳理一下我们团队所在具体业务环境下,升级过程中踩的若干坑,以及一些需要避免的若干注意事项等。

下文所说原先版本,皆指Apisix 1.5,新版则是Apisix 2.2版本。

一、已有服务发现机制无法正常工作

针对上游Upstream没有使用服务发现的路由来讲,本次升级没有遇到什么问题。

公司内部线上业务大都基于Consul KV方式实现服务注册和服务发现,因此我们自行实现了一个 consul_kv.lua 模块实现服务发现流程。

这在Apisix 1.5下面一切工作正常。

但在Apisix 2.2下面,就无法直接工作了,原因如下:

  • 服务发现配置指令变了
  • 上游对象包含服务发现时需增加字段 discovery_type 进行索引

2.1 服务发现配置指令变了

原先运行中仅支持一种服务发现机制,需要配置在 apisix层级下面:

apisix:
    ......
    discover: consul_kv
    ......    

新版需要直接在config*.yaml文件中顶层层级下进行配置,可支持多种不同的路由发现机制,如下:

discovery:                      # service discovery center
  eureka:
    host:                       # it's possible to define multiple eureka hosts addresses of the same eureka cluster.
      - "http://127.0.0.1:8761"
    prefix: "/eureka/"
    fetch_interval: 30          # default 30s
    weight: 100                 # default weight for node
    timeout:
      connect: 2000             # default 2000ms
      send: 2000                # default 2000ms
      read: 5000

我们有所变通,直接在配置文件顶层配置consul_kv多个集群相关参数,避免 discovery 层级过深。

 discovery:
    consul_kv: 1
consul_kv:
  servers:
    -
      host: "172.19.5.30"
      port: 8500
    -
      host: "172.19.5.31"
      port: 8500
  prefix: "upstreams"
  timeout:
    connect: 6000
    read: 6000
    wait: 60
  weight: 1
  delay: 5
  connect_type: "long" # long connect
  ......

当然,这仅仅保证了服务发现模块能够在启动时被正常加载。

推荐阅读:

2.2 upstream对象新增字段discovery_type

Apisix当前同时支持多种服务发现机制,这个很赞。对应的代价,就是需要额外引入 discovery_type 字段,用于索引可能同时存在的多个服务发现机制。

以 Cousul KV方式服务发现为例,那么需要在已有的 upstream 对象中需要添加该字段:

"discovery_type" : "consul_kv"

原先的一个upstream对象,仅仅需要 service_name 字段属性指定服务发现相关地址即可:

{
    "id": "d6c1d325-9003-4217-808d-249aaf52168e",
    "name": "grpc_upstream_hello",
    ......
    "service_name": "http://172.19.5.30:8500/v1/kv/upstreams/grpc/grpc_hello",
    "create_time": 1610437522,
    "desc": "demo grpc service",
    "type": "roundrobin"
}

而新版的则需要添加discovery_type字段,表明该service_name 字段对应的具体模块名称,效果如下:

{
    "id": "d6c1d325-9003-4217-808d-249aaf52168e",
    "name": "grpc_upstream_hello",
    ......
    "service_name": "http://172.19.5.30:8500/v1/kv/upstreams/grpc/grpc_hello",
    "create_time": 1610437522,
    "desc": "demo grpc service",
    "type": "roundrobin",
    "discovery_type":"consul_kv"
}

后面我们若支持Consul Service或ETCD KV方式服务发现机制,则会非常弹性和清晰。

调整了配置指令,添加上述字段之后,后端服务发现其实就已经起作用了。

但gRPC代理路由并不会生效……

二、gRPC当前不支持upstream_id

在我们的系统中,上游和路由是需要单独分开管理的,因此创建的HTTP或GRPC路由需要处理支持upstream_id的索引。

这在1.5版本中,grpc路由是没问题的,但到了apisix 2.2版本中,维护者 @spacewander 暂时没做支持,原因是规划grpc路由和dubbo路由处理逻辑趋于一致,更为紧凑。从维护角度我是认可的,但作为使用者来讲,这就有些不合理了,直接丢弃了针对以往数据的支持。

作为当前Geek一些方式,在 apisix/init.lua 中,最小成本 (优雅和成本成反比)修改如下,找到如下代码:

    -- todo: support upstream id
    api_ctx.matched_upstream = (route.dns_value and
                                route.dns_value.upstream)
                               or route.value.upstream 

直接替换为下面代码即可解决燃眉之急:

    local up_id = route.value.upstream_id
    if up_id then
        local upstreams = core.config.fetch_created_obj("/upstreams")
        if upstreams then
            local upstream = upstreams:get(tostring(up_id))
            if not upstream then
                core.log.error("failed to find upstream by id: " .. up_id)
                return core.response.exit(502)
            end
            if upstream.has_domain then
                local err
                upstream, err = lru_resolved_domain(upstream,
                                                    upstream.modifiedIndex,
                                                    parse_domain_in_up,
                                                    upstream)
                if err then
                    core.log.error("failed to get resolved upstream: ", err)
                    return core.response.exit(500)
                end
            end
            if upstream.value.pass_host then
                api_ctx.pass_host = upstream.value.pass_host
                api_ctx.upstream_host = upstream.value.upstream_host
            end
            core.log.info("parsed upstream: ", core.json.delay_encode(upstream))
            api_ctx.matched_upstream = upstream.dns_value or upstream.value
        end
    else
        api_ctx.matched_upstream = (route.dns_value and
                                route.dns_value.upstream)
                               or route.value.upstream  
    end

三、自定义auth插件需要微调

新版的apisix auth授权插件支持多个授权插件串行执行,这个功能也很赞,但此举导致了先前为具体业务定制的授权插件无法正常工作,这时需要微调一下。

原先调用方式:

    local consumers = core.lrucache.plugin(plugin_name, "consumers_key",
            consumer_conf.conf_version,
            create_consume_cache, consumer_conf)

因为新版的lrucache不再提供 plugin 函数,需要微调一下:

local lrucache = core.lrucache.new({
  type = "plugin",
})
......
    local consumers = lrucache("consumers_key", consumer_conf.conf_version,
        create_consume_cache, consumer_conf)

另一处是,顺利授权之后,需要赋值consumer相关信息:

    ctx.consumer = consumer
    ctx.consumer_id = consumer.consumer_id

此时需要替换成如下方式,为(可能存在的)后续的授权插件继续作用。

consumer_mod.attach_consumer(ctx, consumer, consumer_conf)

更多请参考:apisix/plugins/key-auth.lua 源码。

四、ETCD V2数据迁移到V3

迁移分为三步:

  1. 升级线上已有ETCD 3.3.*版本到3.4.*,满足新版Apisix的要求,这时ETCD实例同时支持了V2和V3格式数据
  2. 迁移V2数据到V3
    • 因为数据量不是非常多,我采取了一个非常简单和原始的方式
    • 使用 etcdctl 完成V2数据到导出
    • 然后使用文本编辑器vim等完成数据的替换,生成etcdctl v3格式的数据导入命令脚本
    • 运行之后V3数据导入脚本,完成V2到V3的数据导入
  3. 修改V3 /apisix/upstreams 中包含服务注册的数据,一一添加 "discovery_type" : "consul_kv"属性

基于以上操作之后,从而完成了ETCD V2到V3的数据迁移。

五、启动apisix后发现ETCD V3已有数据无法加载

我们在运维层面,使用 /usr/local/openresty/bin/openresty -p /usr/local/apisix -g daemon off; 方式运行网关程序。

这也就导致,自动忽略了官方提倡的:apisix start 命令自动提前为ETCD V3初始化的一些键值对内容。

因此,需要提前为ETCD V3建立以下键值对内容:

Key                         Value
/apisix/routes          :   init_dir
/apisix/upstreams       :   init_dir
/apisix/services        :   init_dir
/apisix/plugins         :   init_dir
/apisix/consumers       :   init_dir
/apisix/node_status     :   init_dir
/apisix/ssl             :   init_dir
/apisix/global_rules    :   init_dir
/apisix/stream_routes   :   init_dir
/apisix/proto           :   init_dir
/apisix/plugin_metadata :   init_dir

不提前建立的话,就会导致apisix重启后,无法正常加载ETCD中已有数据。

其实有一个补救措施,需要修改 apisix/init.lua 内容,找到如下代码:

            if not dir_res.nodes then
                dir_res.nodes = {}
            end

比较geek的行为,使用下面代码替换一下即可完成兼容:

                if dir_res.key then
                    dir_res.nodes = { clone_tab(dir_res) }
                else
                    dir_res.nodes = {}
                end

六、apisix-dashboard的支持

我们基于apisix-dashboard定制开发了大量的针对公司实际业务非常实用的企业级特性,但也导致了无法直接升级到最新版的apisix-dashboard。

因为非常基础的上游和路由没有发生多大改变,因此这部分升级的需求可以忽略。

实际上,只是在提交上游表单时,包含服务注册信息JSON字符串中需要增加 discovery_type 字段和对应值即可完成支持。

七、小结

花费了一些时间完成了从Apisix 1.5升级到Apisix 2.2的行为,虽然有些坑,但整体来讲,还算顺利。目前已经上线并全量部署运行,目前运行良好。

针对还停留在Apisix 1.5的用户,新版增加了Control API以及多种服务发现等新特性支持,还是非常值得升级的。

升级之前,不妨仔细阅读每一个版本的升级日志(地址:https://github.com/apache/apisix/blob/2.2/CHANGELOG.md ),然后需要根据具体业务做好兼容测试准备和准备升级步骤,这些都是非常有必要的。

针对我们团队来讲,升级到最新版,一方面降低了版本升级的压力,另一方面也能够辅助我们能参与到开源社区中去,挺好~



nieyong 2021-02-23 14:57 发表评论

参与Apisix开源的一次完整提交过程分享

$
0
0

前言

参与开源不是为了证明什么,而是为了更好的配合工作。开源和工作在绝大部分时间,都是可以和谐共处,互相促进,Win-WIn双赢。

本文内容记录了为 apisix 项目提交的一次pull request提交 (访问地址:https://github.com/apache/apisix/pull/3615 )完整过程,提交内容为一个独立的服务发现模块,本文目的是为团队的其他同学参与社区项目分享的行为提供一个简单可遵循、可操作模型。

概括来讲,简要操作流程如下:

  • 首先,确定需要开源的部分
  • 其次,在项目社区中分享我们的看法和后续行为等
  • 然后,准备提交内容
  • 接着,提交pull request,接受社区审核,反复调整修改
  • 后续,关注社区的走向,持续改进

下面为每一步具体操作的流水账。

提前预警,图多费流量,慎入 :))

首先,我们有一个Consul KV服务发现组件

作为Nginx用户,我们实际场景使用 Nginx Upsync 模块,结合Consul KV作为服务注册和发现形式。

我们基于Apisix构建HTTP API服务网关,没有发现现成的Consul KV形式服务发现模块,既然实际业务需要,我们需要把它按照接口规范开发出来,以适应我们自己的实际场景。

当服务发现模块功能开发出来后,也是仅仅能满足基本需求,还不够完善,但这时改进的思路并不是非常清楚,
既然开源社区也有类似的需求,那我们可以考虑分享开源出去,接收整个社区的考验,大家一起改进。

限于日常思维角度的局限,若是仅仅满足工作需要,那么开源出去会让你的代码接受到社区方方面面的审核,尤其是针对代码风格、功能、执行等有严格要求的apisix项目。摆正心态,接受代码评审并调整,最终结果无疑是让代码更加健壮,好事一桩嘛。

当然开源出去之后,该模块的变更以及优化等行为就完全归属整个社区了,群策群力,是一种比较期待的演进方式。

第一步,咨询社区意见

一个优秀的开源项目,为了稳定健康发展,一般会提供邮件组方便社区参与者咨询、沟通协调等。

一般来说,Github会提供issues列表方便项目使用者提交BUG,若我们想在社区中表达意图、观点等,就不如发在社区邮件组中,这样能够得到更多的关注。比如,我们想给社区共享一个完整的服务发现模块,就可以直接在邮件组中描述大致功能,以及大致处理流程等,让社区知道我们的真实意图。

Apisix开发邮件组地址为:dev@apisix.apache.org,但一般的邮件组都需要注意如下事项:

  1. 沟通需要使用英文
    • 这也是Apisix项目国际化需求
    • 虽然你也知道阅读邮件的有几个中国的糙老爷们,但也会有来自其他国家的用户
    • 当然在Github上所有的项目沟通都需要使用英文,这是一个良好的开源社区沟通习惯
    • 推荐一个微软英语在线协作辅助工具:https://aimwriting.mtutor.engkoo.com/ ,可以帮助校验语法错误等
  2. 无法传递富文本
    • 使用纯文本即可
    • 类似我有格式化强迫症患者,直接粘贴 markdown 格式文本
  3. 无法传递图片

    • 直接传递图片URL地址
    • 若需要传递图片,提供一个小技巧:新建一个issues表单,直接拖拽图片到表单处,然后获得图片地址即可,无须提交issues表单

    上传图片
    上传图片

下面是我发送的邮件截图:


因为apache邮件组不支持富文本和图片,实际看到的效果就没有那么好看了,下面的连接包含了该讨论完整的回复内容:

https://lists.apache.org/thread.html/rf9e392dd76e4701935940d22b4b9d9f8ed130cca34a2e951357a4c2a%40%3Cdev.apisix.apache.org%3E

不方便打开的话,下面提供完整邮件讨论截图,很长的截图,呵呵:

maillist
maillist

总之,断断续续经过三周时间的讨论,这个过程需要有些耐心。发完邮件等有了积极反馈,下面就可以着手准备提交代码了。

第二步,准备提交

Fork到自己仓库

https://github.com/apache/apisix Fork到自己仓库中,然后克隆到自己工作机来。

注意,需要时刻保持和主干保持一致:

git remote add upstream https://github.com/apache/apisix.git

下面就是动手开干了。

按需调整代码

Consul KV服务发现模块文件是 consul_kv.lua,相对位置为:apisix/discovery/consul_kv.lua。我们想提交到项目主干,那么代码就必须遵循已有规范。

针对apisix的服务发现代码,需要有配置项,就必须给出一套完整的服务配置 schema 定义,如下。

local schema = {
    type = "object",
    properties = {
        servers = {
            type = "array",
            minItems = 1,
            items = {
                type = "string",
            }
        },
        fetch_interval = {type = "integer", minimum = 1, default = 3},
        keepalive = {
            type = "boolean",
            default = true
        },
        prefix = {type = "string", default = "upstreams"},
        weight = {type = "integer", minimum = 1, default = 1},
        timeout = {
            type = "object",
            properties = {
                connect = {type = "integer", minimum = 1, default = 2000},
                read = {type = "integer", minimum = 1, default = 2000},
                wait = {type = "integer", minimum = 1, default = 60}
            },
            default = {
                connect = 2000,
                read = 2000,
                wait = 60,
            }
        },
        skip_keys = {
            type = "array",
            minItems = 1,
            items = {
                type = "string",
            }
        },
        default_service = {
            type = "object",
            properties = {
                host = {type = "string"},
                port = {type = "integer"},
                metadata = {
                    type = "object",
                    properties = {
                        fail_timeout = {type = "integer", default = 1},
                        weigth = {type = "integer", default = 1},
                        max_fails = {type = "integer", default = 1}
                    },
                    default = {
                        fail_timeout = 1,
                        weigth = 1,
                        max_fails = 1
                    }
                }
            }
        }
    },

    required = {"servers"}
}

当然,你需要区分每一个配置项是不是必填项,非必传项需要具有默认值,以及上限或下限约束等。

下面需要在该模块启动时进行检测用户配置是否错误,无法兼容、恢复错误的话,需要直接使用Lua内置错误日志接口输出:

error("Errr MSG")

另外,若要引入 resty.worker.events 组件,不要提前require,比如在文件头部提前声明时:

loca  events = require("resty.worker.events")

启动后,就有可能在日志文件中出现如下异常:

2021/02/23 02:32:20 [error] 7#7: init_worker_by_lua error: /usr/local/share/lua/5.1/resty/worker/events.lua:175: attempt to index local 'handler_list' (a nil value)
stack traceback:
    /usr/local/share/lua/5.1/resty/worker/events.lua:175: in function 'do_handlerlist'
    /usr/local/share/lua/5.1/resty/worker/events.lua:215: in function 'do_event_json'
    /usr/local/share/lua/5.1/resty/worker/events.lua:361: in function 'post'
    /usr/local/share/lua/5.1/resty/worker/events.lua:614: in function 'configure'
    /usr/local/apisix/apisix/init.lua:94: in function 'http_init_worker'
    init_worker_by_lua:5: in main chunk

推荐做法是延迟加载,在该模块被加载时进行引用。

local events
local events_list

......

function _M.init_worker()
        ......
        events = require("resty.worker.events")
        events_list = events.event_list(
                "discovery_consul_update_application",
                "updating"
        )
        if 0 ~= ngx.worker.id() then
                events.register(discovery_consul_callback, events_list._source, events_list.updating)
                return
        end
        ......
end        

单元测试依赖

单元测试代码的执行,会在你提交PR代码后自动执行持续集成行为内执行。

首先,需要本机执行单元测试前,需要提前准备好所需Docker测试实例:

docker run --rm --name consul_1 -d -p 8500:8500 consul:1.7 consul agent -server -bootstrap-expect=1 -client 0.0.0.0 -log-level info -data-dir=/consul/data
docker run --rm --name consul_2 -d -p 8600:8500 consul:1.7 consul agent -server -bootstrap-expect=1 -client 0.0.0.0 -log-level info -data-dir=/consul/data

docker run --rm -d \
       -e ETCD_ENABLE_V2=true \
       -e ALLOW_NONE_AUTHENTICATION=yes \
       -e ETCD_ADVERTISE_CLIENT_URLS=http://0.0.0.0:2379 \
       -e ETCD_LISTEN_CLIENT_URLS=http://0.0.0.0:2379 \
       -p 2379:2379 \
    registry.api.weibo.com/wesync/wbgw/etcd:3.4.9

然后,安装项目依赖:

make deps

其次,别忘记在apisix项目持续集成脚本相应位置添加相应依赖。

比如,因为单元测试依赖于端口分别为7500和7600的两个Consul Server实例,需要在执行单元测试之前提前运行,因此你需要在对应的持续集成文件上添加所需运行实例。比如其中一个位置:

无测试不编码

仅仅提供服务发现consul_kv.lua这一个文件,是无法被仓库管理员采纳的,因为除了你自己以外,别人无法确定你提交的代码所提供功能是否足够让人信服,除非你能提供较为完整的 Test::Nginx 单元测试支持,自我证明。

Test::Nginx 单元测试可能针对很多人来讲,是一个拦路虎,但其实有些耐心,你会发现它的美妙之处。

简单入门可参考 https://time.geekbang.org/column/article/109506 (若只需要学习单元测试,其实不需要购买整个专辑的)。在使用过程中需要参考在线文档:https://metacpan.org/pod/Test::Nginx::Socket ,需要一些耐心花费一点时间慢慢消化。

如何运行Nginx单元测试案例,具体参看:
https://github.com/apache/apisix/blob/master/doc/zh-cn/how-to-build.md

至于Apisix定制部分单元测试部分,可以直接参考已有的单元测试文件即可。

Consul KV服务发现的单元测试模块相对路径 t/discovery/consul_kv.lua,在线地址为: https://github.com/apache/apisix/blob/master/t/discovery/consul_kv.t 。该文件大约500多行,比真正的模块consul_kv.lua代码行数还多。但比较完整覆盖了所能想到的所有场景,虽然写起来虽然有些麻烦,但针对应用到线上大量业务的核心代码,无论多认真和谨慎都是不为过的。

以往针对关键核心模块的每一次迭代,心里面大概有些忐忑七上八下吧,也不太敢直接应用到线上。现在有了单元测试各种场景的覆盖辅助验证迭代变更效果,自信心是有了,也可以给别人拍着胸脯保证修改没问题。当然若后续发现隐藏的问题,直接添加上对应的单元测试覆盖上即可。

我们这次只提供一个服务发现模块,因此只需要单独测试consul_kv.t文件即可:

# prove -Itest-nginx/lib -I./ t/discovery/consul_kv.t
......
t/discovery/consul_kv.t .. ok
All tests successful.
Files=1, Tests=102, 36 wallclock secs ( 0.05 usr  0.01 sys +  0.78 cusr  0.41 csys =  1.25 CPU)
Result: PASS

出现测试案例失败问题,可以去 apisix/t/servroot/logs 路径下查看 error.log 文件暴露出的异常等问题。

有些一些测试用例需要组合一组较为复杂的使用场景,比如我们准备一组后端节点:

  • 127.0.0.1:30511,输出 server 1
  • 127.0.0.1:30512,输出 server 2
  • 127.0.0.1:30513,输出 server 3
  • 127.0.0.1:30514,输出 server 4

这些节点将被频繁执行注册Consul节点然后再解除注册若干循环过程:清理注册 -> 注册 -> 解除注册 -> 注册 -> 解除注册 -> 注册 -> 解除注册 -> 注册 ,目的检验已解除注册的失效节点是否还会存在内存中等。

有些操作,比如注册或解除注册节点这些操作,网关的consul_kv.lua服务模块在物理层面需要wait一点时间等待网关消化这些变化,因此我们需要额外提供一个 /sleep 接口,请求时需要故意休眠几秒钟时间等待下一次请求生效。

=== TEST 7: test register & unregister nodes
--- yaml_config eval: $::yaml_config
--- apisix_yaml
routes:
  -
    uri: /*
    upstream:
      service_name: http://127.0.0.1:8500/v1/kv/upstreams/webpages/
      discovery_type: consul_kv
      type: roundrobin
#END
--- config
location /v1/kv {
    proxy_pass http://127.0.0.1:8500;
}
location /sleep {
    content_by_lua_block {
        local args = ngx.req.get_uri_args()
        local sec = args.sec or "2"
        ngx.sleep(tonumber(sec))
        ngx.say("ok")
    }
}
--- timeout: 6
--- request eval
[
    "DELETE /v1/kv/upstreams/webpages/?recurse=true",
    "PUT /v1/kv/upstreams/webpages/127.0.0.1:30511\n" . "{\"weight\": 1, \"max_fails\": 2, \"fail_timeout\": 1}",
    "GET /sleep?sec=5",
    "GET /hello",

    "PUT /v1/kv/upstreams/webpages/127.0.0.1:30512\n" . "{\"weight\": 1, \"max_fails\": 2, \"fail_timeout\": 1}",
    "GET /sleep",
    "GET /hello",
    "GET /hello",

    "DELETE /v1/kv/upstreams/webpages/127.0.0.1:30511",
    "DELETE /v1/kv/upstreams/webpages/127.0.0.1:30512",
    "PUT /v1/kv/upstreams/webpages/127.0.0.1:30513\n" . "{\"weight\": 1, \"max_fails\": 2, \"fail_timeout\": 1}",
    "PUT /v1/kv/upstreams/webpages/127.0.0.1:30514\n" . "{\"weight\": 1, \"max_fails\": 2, \"fail_timeout\": 1}",
    "GET /sleep",

    "GET /hello?random1",
    "GET /hello?random2",
    "GET /hello?random3",
    "GET /hello?random4",

    "DELETE /v1/kv/upstreams/webpages/127.0.0.1:30513",
    "DELETE /v1/kv/upstreams/webpages/127.0.0.1:30514",
    "PUT /v1/kv/upstreams/webpages/127.0.0.1:30511\n" . "{\"weight\": 1, \"max_fails\": 2, \"fail_timeout\": 1}",
    "PUT /v1/kv/upstreams/webpages/127.0.0.1:30512\n" . "{\"weight\": 1, \"max_fails\": 2, \"fail_timeout\": 1}",
    "GET /sleep?sec=5",

    "GET /hello?random1",
    "GET /hello?random2",
    "GET /hello?random3",
    "GET /hello?random4",
]
--- response_body_like eval
[
    qr/true/,
    qr/true/,
    qr/ok\n/,
    qr/server 1\n/,

    qr/true/,
    qr/ok\n/,
    qr/server [1-2]\n/,
    qr/server [1-2]\n/,

    qr/true/,
    qr/true/,
    qr/true/,
    qr/true/,
    qr/ok\n/,

    qr/server [3-4]\n/,
    qr/server [3-4]\n/,
    qr/server [3-4]\n/,
    qr/server [3-4]\n/,

    qr/true/,
    qr/true/,
    qr/true/,
    qr/true/,
    qr/ok\n/,

    qr/server [1-2]\n/,
    qr/server [1-2]\n/,
    qr/server [1-2]\n/,
    qr/server [1-2]\n/
]

准备文档

除了代码能够正常运转,我们还需要准备相应的Markdown文档辅助说明如何使用我们的模块,帮助社区用户更好使用它。

社区一般以英文文档为先, 只有在精力满足的情况下,可以补充中文文档。

下面就是要准备Markdown文档了,其文档路径为:doc/discovery/consul_kv.md,单独的文档需要在其它已有文档挂接上对应链接,方便索引。

文档路径为:doc/discovery/consul_kv.md,在线地址:https://github.com/apache/apisix/blob/master/docs/en/latest/discovery/consul_kv.md

一般建议需要在文档中能够清楚说明模块的使用方式,以及注意事项,尤其是配置参数使用方式等。比如下面的配置项说明:

```yaml
discovery:
  consul_kv:
    servers:
      - "http://127.0.0.1:8500"
      - "http://127.0.0.1:8600"
    prefix: "upstreams"
    skip_keys:                    # if you need to skip special keys
      - "upstreams/unused_api/"
    timeout:
      connect: 1000               # default 2000 ms
      read: 1000                  # default 2000 ms
      wait: 60                    # default 60 sec
    weight: 1                     # default 1
    fetch_interval: 5             # default 3 sec, only take effect for keepalive: false way
    keepalive: true               # default true, use the long pull way to query consul servers
    default_server:               # you can define default server when missing hit
      host: "127.0.0.1"
      port: 20999
      metadata:
        fail_timeout: 1           # default 1 ms
        weight: 1                 # default 1
        max_fails: 1              # default 1
 ```
......

The `keepalive` has two optional values:

- `true`, default and recommend value, use the long pull way to query consul servers
- `false`, not recommend, it would use the short pull way to query consul servers, then you can set the `fetch_interval` for fetch interval

每一个文档都不应该成为信息孤岛,它需要在其它文档上挂载上一个连接地址,因此我们需要在合适的地方,比如需要在 doc/discovery.md最下面添加链接地址描述:

## Discovery modules

- eureka
- [Consul KV](discovery/consul_kv.md)

模块代码,测试文件,以及文档等准备好了之后,下面就是准备提交代码到自己仓库。

验证提交语法规范

所有内容准备好之后,建议执行 make lintmake license-check 两个命令检测代码、markdown文档等是否满足项目规范要求。

# make lint
./utils/check-lua-code-style.sh
+ luacheck -q apisix t/lib
Total: 0 warnings / 0 errors in 133 files
+ find apisix -name '*.lua' '!' -wholename apisix/cli/ngx_tpl.lua -exec ./utils/lj-releng '{}' +
+ grep -E 'ERROR.*.lua:' /tmp/check.log
+ true
+ '[' -s /tmp/error.log ']'
./utils/check-test-code-style.sh
+ find t -name '*.t' -exec grep -E '\-\-\-\s+(SKIP|ONLY|LAST)$' '{}' +
+ true
+ '[' -s /tmp/error.log ']'
+ find t -name '*.t' -exec ./utils/reindex '{}' +
+ grep done. /tmp/check.log
+ true
+ '[' -s /tmp/error.log ']'
# make license-check
.travis/openwhisk-utilities/scancode/scanCode.py --config .travis/ASF-Release.cfg ./
Reading configuration file [.travis/ASF-Release.cfg]...
Scanning files starting at [./]...
All checks passed.

若检查出语法方面问题,认真调整,直到找不到问题所在。

这次PR提交之前,忘记这回事了,会导致多了若干次次submit提交。

第三步,提交Pull Request

去官网:https://github.com/apache/apisix/pulls 新建一个New pull request,后面将使用PR指代pull request

PR标题格式

PR提交标题是规范要求的,模板如下:

{type}: {desc}

其中{type}指代本次PR类型,具体值如下,尽量不要搞错:

  • feat:新功能(feature)
  • fix:修补bug
  • docs:文档(documentation)
  • style: 格式(不影响代码运行的变动)
  • refactor:重构(即不是新增功能,也不是修改bug的代码变动)
  • test:增加测试
  • chore:构建过程或辅助工具的变动
  • ……

其中{desc}需要概括本次提交内容。

比如这次标题为:feat: add consul kv discovery module

填充PR内容

PR内容模板化,为标准的Github Markdown格式,主要目的说明本次提交内容,示范如下:

### What this PR does / why we need it:
<!--- Why is this change required? What problem does it solve? -->
<!--- If it fixes an open issue, please link to the issue here. -->

### Pre-submission checklist:

* [ ] Did you explain what problem does this PR solve? Or what new features have been added?
* [ ] Have you added corresponding test cases?
* [ ] Have you modified the corresponding document?
* [ ] Is this PR backward compatible? **If it is not backward compatible, please discuss on the [mailing list](https://github.com/apache/apisix/tree/master#community) first**

按照模板格式填写,省心省力,如下:

### What this PR does / why we need it:

As I  mentioned previously in the mail-list, my team submit our `consul_kv` discovery module now.

More introductions here: 
 https://github.com/yongboy/apisix/blob/consul_kv/doc/discovery/consul_kv.md

### Pre-submission checklist:

* [x] Did you explain what problem does this PR solve? Or what new features have been added?
* [x] Have you added corresponding test cases?
* [x] Have you modified the corresponding document?
* [x] Is this PR backward compatible? **If it is not backward compatible, please discuss on the [mailing list](https://github.com/apache/apisix/tree/master#community) first**

认真接受评审和建议

提交PR之后,才是一个开始,起点。

Apisix项目会自动针对我们所提交内容执行持续集成,apisix项目的检查项很多,比如针对Markdown格式就很严格:

持续集成不通过,按照要求微调吧,也是标准化的要求。

我们在PUSH代码之前,使用 make lintmake license-check 两个命令提前检测还是十分有必要的,提前检测语法等。

首先,一定要确保持续集成不能出错。持续集成通不过,说明我们的准备还不充分,继续调整修改,继续提交,一直到持续集成完全执行成功为止。

保证持续集成执行成功,这是最基本的要求,否则社区无法确认我们的代码是否基本合格。

放松心态,准备开始改进BUG,以及接受社区的各种代码评审和改进意见吧。

其次,就是要虚心接受社区代码评审和改进意见了,这是最关键的一步。

下面是一些建议:

  1. 真正代码BUG,认真修改
  2. 逻辑处理不合理的地方,思考并给出一些处理思路,确定好之后开始调整即可
  3. 有些提议可能会超出本次提交范围,说明原因,给出拒绝理由,可以婉拒嘛,比如可以放在下一次的提交中。
  4. 若有遇到自己处理不了的问题,积极向社区寻求帮助吧。
  5. 针对一批次修改再次提交后,会再次执行持续集成,一样确保持续集成不能够失败,然后继续等待下一轮的审核

认真对待每一个建议,有则改之无则加勉,不知不觉之间就进步了很多,代码质量也得到了提升。

经过多次的微调,我们的服务发现核心模块基本上已趋于完善了一版,这已经和还没准备分享出来之前的原始文件相比已经天差地别了 :))

下面是本次PR包含的多次提交、代码评审以及答复等完整流程截图:

consul_kv模块一次PR完整流程
consul_kv模块一次PR完整流程

被合并到主分支之后,有没有感觉到整个社区都在帮助我们一起改进,快不快哉 ?

关于依赖项的处理

本次提交的服务发现模块依赖一个组件:lua-resty-consul,其仓库地址:https://github.com/hamishforbes/lua-resty-consul,最新版本为:`0.3.2`。因为我们在实际部署定制时,直接下载了该文件,简单直接粗暴。

apisix项目针对项目依赖,采用的 LuaRocks 管理,在 2021-2-20 之前该组件托管在 https://luarocks.org/modules/hamish/lua-resty-consul 上面最新版本为 0.2-0,这就很难办了。

我的处理步骤如下:

  1. 首先我在github上面向作者提交一个求助:https://github.com/hamishforbes/lua-resty-consul/issues/20, 然而并没有在一两周时间内没有等到作者回复
  2. 无奈,只好自己在 LuaRocks 单独提交一个暂时性的解决方案:https://luarocks.org/modules/yongboy/lua-resty-consul-0.3.2,在本次PR中直接包含了该组件临时地址
  3. 三周左右,终于等到该组件作者提交最新版到 LuaRocks 站点,既然官方更新了,那就把服务发现模块里面的依赖修改为官方最新地址吧,再次提价一个PR:https://github.com/apache/apisix/pull/3654

有些一波三折 :))

第四步,关于后续

一旦合并到主分支后,后续的演进整个社区都可以参与进来,可能有人提 issue,可能有人提 PR 修改等,后续我们想为该模块继续提交,那将是另外一个PR的事情。

我们可以继续做以下事情:

  1. 根据实际需要重构
  2. 若有人提Issue是,自然是Fixbug;实践中遇到的Bug,修复它
  3. 需要添加新的单元测试覆盖到新的特性
  4. 若有需要,就需要添加新的文档进行描述

毫无疑问,这是一个良性循环。

小结

参与社区开发的其它类型提交,可能会比上面所述简单很多,但大都可以看做是以上行为的一个子集。

参与开源,也会为我们打开一扇窗户,去除自身的狭隘。积极向社区靠拢,这需要磨去一些思维或认知的棱角,虚心认识到自我的不足,并不断调整不断进步。

加油!



nieyong 2021-03-10 11:20 发表评论

K8S 修改默认 StorageClass

$
0
0

业务需要将默认的K8S存储服务修改NFS,这里记录一下操作记录。

列出当前StorageClass

kubectl get sc
NAME                   PROVISIONER                                         RECLAIMPOLICY   VOLUMEBINDINGMODE      ALLOWVOLUMEEXPANSION   AGE
local-path (default)   rancher.io/local-path                               Delete          WaitForFirstConsumer   false                  17d
nfs                    cluster.local/nfs-nfs-subdir-external-provisioner   Delete          Immediate              true                   6d14h

首先,将默认的名称为local-path修改为false

kubectl patch storageclass local-path -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"false"}}}'

然后,将nfs设置为默认:

kubectl patch storageclass nfs -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'

操作完成之后,校验一下,可以看到已经成功将nfs设置为默认的StorageClass选项。

kubectl get sc
NAME            PROVISIONER                                         RECLAIMPOLICY   VOLUMEBINDINGMODE      ALLOWVOLUMEEXPANSION   AGE
local-path      rancher.io/local-path                               Delete          WaitForFirstConsumer   false                  17d
nfs (default)   cluster.local/nfs-nfs-subdir-external-provisioner   Delete          Immediate              true                   6d14h

Ref:https://kubernetes.io/docs/tasks/administer-cluster/change-default-storage-class/



nieyong 2023-09-08 15:30 发表评论

K8S Service 多种类型 Types 测试手记

$
0
0

前言

这里基于whoami示范服务,部署3个实例,分别一一验证各种类型的K8S Service服务范畴。

大致逐一从下面列表逐一验证每种类型的Service访问方式:

  • Service Name
  • 域名解析结果等
  • CLUSTER-IP
  • EXTERNAL-IP

一些设定如下:

  • 测试环境K8S版本号为v1.27.3
  • K8S集群Node节点IP地址段范围:10.0.1.0/24
  • K8S集群自动生成Pod网段为10.43.0.0/24
  • 本书所列代码皆可拷贝直接粘贴到终端界面直接运行

首先,部署whoami服务

先部署包含3个实例的whoami

# cat << 'EOF' | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: whoami
  labels:
    app: whoami
spec:
  replicas: 3
  selector:
    matchLabels:
      app: whoami
  template:
    metadata:
      labels:
        app: whoami
    spec:
      containers:
      - name: whoami
        image: containous/whoami
        ports:
        - containerPort: 80
          name: web
EOF

查看一下:

# kubectl get all
NAME                                                      READY   STATUS      RESTARTS         AGE
pod/whoami-767d459f67-qffqw                               1/1     Running     0                23m
pod/whoami-767d459f67-xdv9p                               1/1     Running     0                23m
pod/whoami-767d459f67-gwpgx                               1/1     Running     0                23m

NAME                                                  READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/whoami                                3/3     3            3           23m

NAME                                                            DESIRED   CURRENT   READY   AGE
replicaset.apps/whoami-767d459f67                               3         3         3       23m

其次,安装busybox进行调试

安装一个包含有curl的busybox方便后续调试:

kubectl run busybox-curl --image=yauritux/busybox-curl --command -- sleep 3600

另起一个终端,输入下面命令进入:

kubectl exec -ti busybox-curl -n default -- sh

环境准备好之后,下面逐一测试各种类型:

默认Cluster IP模式

K8S默认Service为Cluster IP模式,面向内部Pod以及通过Ingress对外提供服务。

下面一张图很清晰解释清楚了PortTargetPort适用情景,Port为Service对外输出的端口,TargetPort为服务后端Pod的端口,两者之间有一个转换:port -> targetPort -> containerPort

创建一个Service:

cat << 'EOF' | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
  labels:
    name: whoami-clusterip
  name: whoami-clusterip
spec:
  ports:
  - port: 80
    targetPort: 80
    protocol: TCP
  selector:
    app: whoami
EOF

部署后可以查看一下:

NAME                          TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
service/whoami-clusterip      ClusterIP      10.43.247.74    <none>        80/TCP         57s

下面就需要逐一测试了。

域名形式:

# curl whoami-clusterip
Hostname: whoami-767d459f67-gwpgx
IP: 127.0.0.1
IP: 10.42.8.35
RemoteAddr: 10.42.9.32:35968
GET / HTTP/1.1
Host: whoami-clusterip
User-Agent: curl/7.81.0
Accept: */*

Cluster IP形式:

# curl 10.43.247.74
Hostname: whoami-767d459f67-qffqw
IP: 127.0.0.1
IP: 10.42.3.73
RemoteAddr: 10.42.9.32:42398
GET / HTTP/1.1
Host: 10.43.247.74
User-Agent: curl/7.81.0
Accept: */*

域名解析,只解析到Cluster IP上:

# nslookup whoami-clusterip
Server:		10.43.0.10
Address:	10.43.0.10:53

Name:	whoami-clusterip.default.svc.cluster.local
Address: 10.43.247.74

External IP模式

原理同Cluster IP模式,为指定服务绑定一个额外的一个IP地址。当终端访问该IP地址,将流量一样转发到Service。

当访问external IP,其端口转换过程:port -> targetPort -> containerPort

与默认Service相比,端口转换流程没有增加,但好处对外暴露了一个可访问的IP地址,不过可能需要在交换机/路由器层面提供动静态路由支持。

cat << 'EOF' | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
  labels:
    name: whoami-externalip
  name: whoami-externalip
spec:
  ports:
  - port: 80
    targetPort: 80
    protocol: TCP
  selector:
    app: whoami
  externalIPs:
  - 10.10.10.10
EOF

服务显示如下,绑定了指定的扩展IP地址10.10.10.10

# NAME                          TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
service/whoami-externalip     ClusterIP      10.43.192.118   10.10.10.10   80/TCP         57s

kube-proxy 将在每一个Node节点为10.10.10.10上建立一个转发规则,该IP地址的80端口将直接转发到对应的后端三个whoami Pod 上。

-A KUBE-SERVICES -d 10.10.10.10/32 -p tcp -m comment --comment "default/whoami-externalip external IP" -m tcp --dport 80 -j KUBE-EXT-QN5HIEVYUPDP6UNK

......
-A KUBE-EXT-QN5HIEVYUPDP6UNK -j KUBE-SVC-QN5HIEVYUPDP6UNK
......

-A KUBE-SVC-QN5HIEVYUPDP6UNK ! -s 10.42.0.0/16 -d 10.43.192.118/32 -p tcp -m comment --comment "default/whoami-externalip cluster IP" -m tcp --dport 80 -j KUBE-MARK-MASQ
-A KUBE-SVC-QN5HIEVYUPDP6UNK -m comment --comment "default/whoami-externalip -> 10.42.2.79:80" -m statistic --mode random --probability 0.33333333349 -j KUBE-SEP-JSAT6D2KFCSF4YLF
-A KUBE-SVC-QN5HIEVYUPDP6UNK -m comment --comment "default/whoami-externalip -> 10.42.3.77:80" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-2R66UI3G2AY2IMNM
-A KUBE-SVC-QN5HIEVYUPDP6UNK -m comment --comment "default/whoami-externalip -> 10.42.8.42:80" -j KUBE-SEP-ZHHIL2SAN2G37GCM

访问域名:

# curl whoami-externalip
Hostname: whoami-767d459f67-gwpgx
IP: 127.0.0.1
IP: 10.42.8.35
RemoteAddr: 10.42.9.32:46746
GET / HTTP/1.1
Host: whoami-externalip
User-Agent: curl/7.81.0
Accept: */*

访问ClusterIP形式:

# curl 10.43.192.118
Hostname: whoami-767d459f67-qffqw
IP: 127.0.0.1
IP: 10.42.3.73
RemoteAddr: 10.42.9.32:47516
GET / HTTP/1.1
Host: 10.43.192.118
User-Agent: curl/7.81.0
Accept: */*

访问暴露的External IP:

# curl 10.10.10.10
Hostname: whoami-767d459f67-gwpgx
IP: 127.0.0.1
IP: 10.42.8.35
RemoteAddr: 10.42.9.0:38477
GET / HTTP/1.1
Host: 10.10.10.10
User-Agent: curl/7.81.0
Accept: */*

域名解析结果只解析到其对应的Cluster IP:

# nslookup whoami-externalip
Server:		10.43.0.10
Address:	10.43.0.10:53

Name:	whoami-externalip.default.svc.cluster.local
Address: 10.43.192.118

NodePort 模式

Cluster IP相比,多了一个nodePort,这个NodePort会在K8S所有Node节点上都会开放。

这里有一个端口转换过程:nodePort -> port -> targetPort -> containerPort,多了一层数据转换过程。

服务定义如下:

cat << 'EOF' | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
  labels:
    name: whoami-nodeport
  name: whoami-nodeport
spec:
  type: NodePort
  ports:
  - port: 80
    targetPort: 80
    nodePort: 30080
    protocol: TCP
  selector:
    app: whoami
EOF

查看一下服务分配地址:

NAME                          TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
service/whoami-nodeport       NodePort       10.43.215.233   <none>        80:30080/TCP   57s

访问域名:

# curl whoami-nodeport
Hostname: whoami-767d459f67-xdv9p
IP: 127.0.0.1
IP: 10.42.2.75
RemoteAddr: 10.42.9.32:36878
GET / HTTP/1.1
Host: whoami-nodeport
User-Agent: curl/7.81.0
Accept: */*

测试 CLUSTER IP :

# curl 10.43.215.233
Hostname: whoami-767d459f67-qffqw
IP: 127.0.0.1
IP: 10.42.3.73
RemoteAddr: 10.42.9.32:40552
GET / HTTP/1.1
Host: 10.43.215.233
User-Agent: curl/7.81.0
Accept: */*

因为是在每一个K8S Node节点上都会开放一个30080端口,因此可以这样访问 {Node IP}:{nodePort},如下Node IP地址为10.0.1.11

# curl 10.0.1.11:30080
Hostname: whoami-767d459f67-qffqw
IP: 127.0.0.1
IP: 10.42.3.73
RemoteAddr: 10.42.1.0:1880
GET / HTTP/1.1
Host: 10.0.1.11:30080
User-Agent: curl/7.81.0
Accept: */*

域名还是只解析到对应Cluster IP:

# nslookup whoami-nodeport
Server:		10.43.0.10
Address:	10.43.0.10:53

Name:	whoami-nodeport.default.svc.cluster.local
Address: 10.43.215.233

LoadBalancer 模式

LoadBalancer模式,会强制K8S Service自动开启nodePort

这里有一张图,详细解析数据流向。

服务数据端口转换过程:port -> nodePort -> port -> targetPort -> containerPort

  • 与默认Cluster IP相比,多了两层数据转换过程
  • nodePort相比,对了一层数据转换过程
  • externalIP相比,在小流量场景下就没有什么优势了

具体服务定义:

cat << 'EOF' | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
  labels:
    name: whoami-clusterip-none
  name: whoami-clusterip-none
spec:
  clusterIP: None
  ports:
  - port: 80
    targetPort: 80
    protocol: TCP
  selector:
    app: whoami
EOF

查看一下部署结果:

NAME                          TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
service/whoami-loadbalancer   LoadBalancer   10.43.63.92     <pending>     80:30906/TCP   57s

服务域名形式:

# curl whoami-loadbalancer
Hostname: whoami-767d459f67-qffqw
IP: 127.0.0.1
IP: 10.42.3.73
RemoteAddr: 10.42.9.32:57844
GET / HTTP/1.1
Host: whoami-loadbalancer
User-Agent: curl/7.81.0
Accept: */*

测试 CLUSTER-IP

# curl 10.43.63.92
Hostname: whoami-767d459f67-xdv9p
IP: 127.0.0.1
IP: 10.42.2.75
RemoteAddr: 10.42.9.32:42400
GET / HTTP/1.1
Host: 10.43.63.92
User-Agent: curl/7.81.0
Accept: */*

域名解析到Cluster IP:

#  nslookup whoami-loadbalancer
Server:		10.43.0.10
Address:	10.43.0.10:53

Name:	whoami-loadbalancer.default.svc.cluster.local
Address: 10.43.63.92

安装LoadBalancer

此时whoami-loadbalancer服务对应的EXTERNAL-IP<pending>,我们需要安装一个负载均衡器,可以选择MetalLB作为负载均衡器。

# kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.13.11/config/manifests/metallb-native.yaml

稍后分配可用的LoadBalaner可分配的地址池:

cat << 'EOF' | kubectl apply -f -
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: default-pool
  namespace: metallb-system
spec:
  addresses:
  - 10.0.1.100-10.0.1.200
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: default
  namespace: metallb-system
spec:
  ipAddressPools:
  - default-pool
EOF

等安装完成之后,可以看到服务whoami-loadbalancer分配的IP地址为 10.0.1.101

NAME                          TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
......
service/whoami-loadbalancer LoadBalancer   10.43.63.92     10.0.1.101         80:30906/TCP   27h
......

测试负载均衡IP地址

测试一下:

# curl 10.0.1.101
Hostname: whoami-767d459f67-xdv9p
IP: 127.0.0.1
IP: 10.42.2.78
RemoteAddr: 10.42.8.0:33658
GET / HTTP/1.1
Host: 10.0.1.101
User-Agent: curl/7.79.1
Accept: */*

我们看到该服务分配的端口为80:30906/TCP30906为K8S为该服务自动生成的NodePort类型端口。

可以找任一K8S Node节点IP地址测试一下:

# curl 10.0.1.12:30906
Hostname: whoami-767d459f67-qffqw
IP: 127.0.0.1
IP: 10.42.3.77
RemoteAddr: 10.42.2.0:9717
GET / HTTP/1.1
Host: 10.0.1.12:30906
User-Agent: curl/7.81.0
Accept: */*

分析一下路由表,可以分析到该负载均衡的External_IP:80的打流量到NodePort:30906上,然后走Service对应{Pod:80}流量分发逻辑。

-A KUBE-NODEPORTS -p tcp -m comment --comment "default/whoami-loadbalancer" -m tcp --dport 30906 -j KUBE-EXT-NBTYBEEXACZI7DPC

......

-A KUBE-SERVICES -d 10.0.1.101/32 -p tcp -m comment --comment "default/whoami-loadbalancer loadbalancer IP" -m tcp --dport 80 -j KUBE-EXT-NBTYBEEXACZI7DPC

......

-A KUBE-EXT-NBTYBEEXACZI7DPC -m comment --comment "masquerade traffic for default/whoami-loadbalancer external destinations" -j KUBE-MARK-MASQ
-A KUBE-EXT-NBTYBEEXACZI7DPC -j KUBE-SVC-NBTYBEEXACZI7DPC

......

-A KUBE-SVC-NBTYBEEXACZI7DPC ! -s 10.42.0.0/16 -d 10.43.63.92/32 -p tcp -m comment --comment "default/whoami-loadbalancer cluster IP" -m tcp --dport 80 -j KUBE-MARK-MASQ
-A KUBE-SVC-NBTYBEEXACZI7DPC -m comment --comment "default/whoami-loadbalancer -> 10.42.2.79:80" -m statistic --mode random --probability 0.33333333349 -j KUBE-SEP-E3K3SUYNFWT2VICE
-A KUBE-SVC-NBTYBEEXACZI7DPC -m comment --comment "default/whoami-loadbalancer -> 10.42.3.77:80" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-HG5MYVVID7GJOZA7
-A KUBE-SVC-NBTYBEEXACZI7DPC -m comment --comment "default/whoami-loadbalancer -> 10.42.8.42:80" -j KUBE-SEP-GFJH72YCBKBFB6OG

Headless 无头模式

一般应用在有状态的服务,或需要终端调用者自己实现负载均衡,等一些特定场景。

通过调用者从端口角度分析,数据转换流程:targetPort -> containerPort

在意服务性能的场景,不妨试试无头模式。


服务定义:

cat << 'EOF' | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
  labels:
    name: whoami-clusterip-none
  name: whoami-clusterip-none
spec:
  clusterIP: None
  ports:
  - port: 80
    targetPort: 80
    protocol: TCP
  selector:
    app: whoami
EOF

查看服务部署情况:

NAME                          TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
service/whoami-clusterip-none   ClusterIP      None            <none>                     80/TCP         9h

通过service域名访问,K8S会自动根据服务域名whoami-clusterip-none进行pick后端对应Pod IP地址。

# curl whoami-clusterip-none
Hostname: whoami-767d459f67-xdv9p
IP: 127.0.0.1
IP: 10.42.2.75
RemoteAddr: 10.42.9.32:34998
GET / HTTP/1.1
Host: whoami-clusterip-none
User-Agent: curl/7.81.0
Accept: */*

查询DNS会把所有节点都列出来。

# nslookup whoami-clusterip-none
Server:		10.43.0.10
Address:	10.43.0.10:53

Name:	whoami-clusterip-none.default.svc.cluster.local
Address: 10.42.3.73
Name:	whoami-clusterip-none.default.svc.cluster.local
Address: 10.42.2.75
Name:	whoami-clusterip-none.default.svc.cluster.local
Address: 10.42.8.35

External Name模式

用于引进带域名的外部服务,这里引入内部服务作为测试。

多了一层域名解析过程,端口转换流程依赖于所引入服务的服务设定。

服务定义:

cat << 'EOF' | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
  labels:
    name: whoami-externalname
  name: whoami-externalname
spec:
  type: ExternalName
  externalName: whoami-clusterip.default.svc.cluster.local
EOF

这里外联的是whoami-clusterip服务的完整访问域名。

查看服务部署情况:

NAME                          TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
service/whoami-externalname     ExternalName   <none>          whoami-clusterip.default   <none>         9h

根据域名访问测试:

# curl whoami-externalname
Hostname: whoami-767d459f67-qffqw
IP: 127.0.0.1
IP: 10.42.3.77
RemoteAddr: 10.42.9.35:36756
GET / HTTP/1.1
Host: whoami-externalname
User-Agent: curl/7.81.0
Accept: */*

DNS解析结果:

# nslookup whoami-externalname
Server:		10.43.0.10
Address:	10.43.0.10:53

whoami-externalname.default.svc.cluster.local	canonical name = whoami-clusterip.default.svc.cluster.local
Name:	whoami-clusterip.default.svc.cluster.local
Address: 10.43.247.74

小结

简要分析了各种类型Service定义、服务引用场景以及测试流程等,整理清楚了,也方便在具体业务场景中进行抉择选择具体服务类型。



nieyong 2023-09-13 10:13 发表评论
Viewing all 30 articles
Browse latest View live