zookeeper密码权限验证

假定使用账户密码为panxie:123456,那么zookeeper生成的鉴权字段为BASE64(SHA1(panxie+123456)),使用zookeeper自带的工具生成鉴权字段auth,命令为:

java -Djava.ext.dirs=/home/oicq/zkcluster/zk1/lib -cp /home/oicq/zkcluster/zk1/zookeeper-3.4.14.jar org.apache.zookeeper.server.auth.DigestAuthenticationProvider panxie:123456

输出结果为:

panxie:123456->panxie:ls2Y30BnQBe3KJYoYp1ylpd7oTo=

对于不存在的节点,使用create创建节点时指定auth,命令为:

create "/auth_test" "hello,world" digest:panxie:ls2Y30BnQBe3KJYoYp1ylpd7oTo=:cdrwa

最后的cdrwa对应的权限表格如下。

至此,访问/auth_test节点之前必须使用命令addauth来完成认证,否则会提示鉴权无效。

[zk: 127.0.0.1:2283(CONNECTED) 3] get /auth_test
Authentication is not valid : /auth_test

addauth使用方法如下:

addauth digest panxie:123456

完后认证后再次访问时成功。

[zk: 127.0.0.1:2283(CONNECTED) 8] get /auth_test                                                   
hello,world
cZxid = 0x1000000e54
ctime = Tue Nov 03 10:05:25 CST 2020
mZxid = 0x1000000e54
mtime = Tue Nov 03 10:05:25 CST 2020
pZxid = 0x1000000e54
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 11
numChildren = 0

golang json.Unmarshal()不清除结构体原有变量的值

背景
在某项目中,由于技术升级或者提升产品适用场景,需要对寻址参数进行修改,公司内寻址用的modid和cmdid两个整数统一使用字符串addr替代,便于兼容ipvs等。
为减少兼容代码,存储在zookeeper中的存量配置数据需要做结构转换,这活我干了。

故障代码

多么简单单纯的代码,每天都这样搬砖,职业生涯是道不了35岁的,也许30岁就可以退休了。

func (act *AccessCfgsTool) convert() {
	// 读出Task信息后,清理掉L5的两个整数后写回
        var task mc_proto.Task
	for _, v := range act.taskIDs {		
		//处理access_cfg配置
		path := fmt.Sprintf("%s/%d", TASK_PATH, v)
		fmt.Printf("converting %s.\n", path)
		_, err := zkutil.ZkReadJson(act.conn.conn, path, &task)		
		//做转换处理 XXXXXXXX
		err = zkutil.ZkUpdateJson(act.conn.conn, path, -1, &task)		
		fmt.Printf("convert %s done.\n", path)
	}
}

故障现象

在测试环境上线后,过了几天,一位大哥跟我说,数据怎么是乱的,为什么这个任务的配置上,会有其他任务的信息?

我蒙蔽了,但是无知让我无畏:怎么可能,一定是你们做了什么操作,搞乱了。
外强中干的我赶紧看看转换代码,发现了可能的原因,并简单的测试了一下。

func TestUnMarshal() {
   type T struct {
      A string
      B string
   }
   t := T{A: "hello,world,A"}
   str := "{\"B\":\"hello,world,B\"}"
   json.Unmarshal([]byte(str), &t)
   fmt.Printf("%v\n", t)
}

D:\xxx\src\paper>go run main.go
{hello,world,A hello,world,B}

故障原因

到这里大概都知道故障的原因了,对转换代码做下修正。

func (act *AccessCfgsTool) convert() {
	// 读出Task信息后,清理掉L5的两个整数后写回
        //移动到下面第二行 var task mc_proto.Task
	for _, v := range act.taskIDs {		
                var task mc_proto.Task
		//处理access_cfg配置
		path := fmt.Sprintf("%s/%d", TASK_PATH, v)
		fmt.Printf("converting %s.\n", path)
		_, err := zkutil.ZkReadJson(act.conn.conn, path, &task)		
		//做转换处理 XXXXXXXX
		err = zkutil.ZkUpdateJson(act.conn.conn, path, -1, &task)		
		fmt.Printf("convert %s done.\n", path)
	}
}

问题解决了。

结论

golang json.Unmarshal()不会替我们清除已有变量的值,只会用解析出的值进行覆盖赋值。

zookeeper Watch丢通知故障的定位

在下面的描述中,ZK指的是zookeeper,Watch丢通知故障简称为丢消息,因个人水平的原因,文章中定位出的原因,未必是真实的原因,仅供参考。

背景介绍

在我深度参与的一个计算平台项目中,团队第一次使用ZK作为配置中心,ZK的功能:(1)存储和固化配置;(2)在配置发生更新的时候,通知多个工作节点拉取新的配置。这两项是ZK作为分布式服务框架最常用的功能之一。项目使用github上开源的go-zookeeper库来实现ZK的操作,库地址为:github.com/samuel/go-zookeeper/zk

工作节点任务的升级,依赖于ZK通过Watch消息通知给客户端代理,以下简称agent,agent是由我开发维护的模块。ZK一共3个节点,按照IP最后的数字,分别命名为144、227、229。故事发生在agent和三台ZK服务器之间。

系统简化结构

故障现象

用户在客户端执行一些配置更新后,经常反馈计算节点的配置没有更新成功,还在跑着旧版本。登录计算节点查看日志,可以发现在用户执行更新后的几分钟内,Agent没有进入任何通知消息的回调处理。故障的紧急恢复采用的方法是重启agent,重启后会全量拉取新的配置。另外注意到,重启后一段时间内(在几个小时到几天不等),可以正常收到ZK的消息。

定位过程

首先简单介绍代码。在zk.Connect连接上conf.ZkHost中的某一台ZK节点后,在go-zookeeper的sendLoop中会按照指定的时间间隔,由agent主动发起ping操作并等待应答。没有收到或者收到错误的应答之后,连接将被关闭,并且在一个for循环中主动去尝试conf.ZkHost中的其他节点,如果迅速恢复,使用的sessionid不发生变化。

//连接ZK服务器,注册回调函数
if agent.zkConn, _, err = zk.Connect(conf.ZkHost, time.Second, 
                           zk.WithEventCallback(ZkCallback), 
                           zk.WithMaxBufferSize(10*1024*1024), 
                           zk.WithMaxConnBufferSize(10*1024*1024)); 
                           err != nil {
   Logger.LogError("connect %v err, %s", conf.ZkHost, err)  
}
//ZK回调函数
func ZkCallback(ev zk.Event) {
	go agent.HandleEvent(ev)
}
//ZK消息处理函数
func (m *McAgent) HandleEvent(ev zk.Event) {	
	switch ev.Type {
	case zk.EventNodeChildrenChanged:
		//
	case zk.EventNodeDataChanged:
		//
	case zk.EventNodeDeleted:
		//	
	}
}

根据代码,结合go-zookeeper实现,发现了第一个问题:没有处理ZK的状态变化消息。在agent与ZK节点之间由于网络偶发故障或延时,导致Agent ping ZK节点失败的时候,ZK会连接其他ZK节点,这时候之前通过GetW方式注册的Watch事件会全部丢失。导致这个Agent再也收不到原先监控的ZK节点变化的消息。

针对这个故障,考虑到在网络故障的短暂时间内存在丢消息的可能,因此解决方案比较直接:

func (m *McAgent) HandleEvent(ev zk.Event) {
   
   switch ev.Type {
   case zk.EventNodeChildrenChanged:
      //
   case zk.EventNodeDataChanged:
      //
   case zk.EventNodeDeleted:
     //
   case zk.EventSession:
      if ev.State == zk.StateExpired {
        //继续使用本地缓存的数据,可能是脏的
        //打告警
      } else if ev.State == zk.StateConnected {
        //从ZK全量拉取新数据,重新注册Watch
      }
   }
}

作为一个严谨求实的程序员,本着对自己代码高度负责任的态度,我使用iptables模拟网络断开重连的情况,能及时的给出告警,并且可以自动重连,重连之后还能实时收到ZK服务器发出的事件通知。

iptables -A OUTPUT -p tcp --dport 2181 -j DROP
iptables -D OUTPUT -p tcp --dport 2181 -j DROP

更新上线,平静了好几天,没人找我说怎么任务又更新失败了。完美。

在我以为这事终于消停之后,又偶尔有用户在群里怼我,说有更新失败的情况,但是我没有收到任何告警信息。感觉颜面扫地,年底可以one星走人了。不过离年底还有点时间,先再找找原因。

从故障Agent的日志看,没有任何异常,也没有任何ZK连接变化相关的日志信息。去ZK节点上捞取日志,通过一系列检索过程,发现了故障场景的共性。

故障相关的ZK节点日志

日志不太直观,下面的图形更直观一些。简单的说,就是Agent所连接的ZK服务器,在静默的情况下,由一台(144)迅速迁移到了另一台(227),使用相同的sessionid重建与新服务器的连接。由于客户端注册的Watch在ZK服务器上是以本地存储的方式记录,并没有同步给其他机器。因此,在连接静默迁移到新的服务器之后,Watch又丢掉了。所谓静默迁移,就是agent端没有收到连接变化相关的任何回调消息。这可能是go-zookeeper的bug,但是时间精力原因,没有继续深入下去了。

故障过程描述

解决方案

从ZK相关的文档可以看出,ZK消息并不保证一定送达,在网络短暂故障的重连期间,仍然可能存在消息丢失的情况,所以在ZK服务器压力不大并且数据不大的情况下,彻底的解决方案是废弃Watch机制不用,周期性全量拉取ZK数据。

使用定期全量拉取数据之后,类似问题再也没有出现过。

优化空间

在ZK节点数量比较多、节点数据比较大的情况下,大量重新拉去数据会给ZK服务器和网络带来压力。准备执行的优化是设置一个节点,存储数据版本信息,在数据版本未发生变化的时候,不执行重新拉取的操作。

还有一个方向是去调试go-zookeeper代码,比较简单的方式是在其中起连接ZK IP的监控代码,在调用Next函数的时候,使用setState方式发送一下连接变化的消息。但是由于通过网络传输的消息存在丢失的可能,这仍然不是最终的解决方法。

再次强调,相关结论存在模糊和不清楚的地方,不要轻信。有更好方法的,请留言告知。

go结构体中的匿名变量在json.marshal中隐藏的坑

故障背景

在昨天的工作中,遇到一个诡异的小问题,调试了一段时间,在网上也没有找到相关材料(可能谷歌能力有限,搜索不到,要用百度)。先来看一段简单的简化后的代码,简单、清晰、明了、接地气,与我的工作场景一致。

/*
 * Copyright (c) 2020.
 * panxie@tencent.com
 */

package main

import (
   "encoding/json"
   "fmt"
)

type L5 struct {
   Modid int64 `json:"modid"`
   Cmdid int64 `json:"cmdid"`
}

type CommonAttr struct {
   Name string `json:"name"`
   Desc string `json:"desc"`
}

type OperationQueryCKV struct {
   L5       
   Expression string `json:"expression"`
   TimeoutMs  int64  `json:"timeout_ms"`
}

type Operation struct {
   CommonAttr
   *OperationQueryCKV
}

func main() {
   op := &Operation{
      CommonAttr{
         Name: "op_name",
         Desc: "op_desc",
      },
      &OperationQueryCKV{
         L5: L5{
            Modid: 12345,
            Cmdid: 54321,
         },
         Expression: "expression",
         TimeoutMs:  80,
      },
   }
   b, _ := json.MarshalIndent(op,"","\t")
   fmt.Printf("%s\n", string(b))
}

关注一下L5结构,这是一种公司内部服务负载均衡和服务发现的公共件,简单的说,一个L5可以换到一个IP和端口列表,然后拿去访问服务。对于CKV查询功能来说,需要一个L5表明CKV在哪。

漫天飞舞的匿名字段,是不是像golang大神特有的标签。运行一下,没问题,符合预期,完美。

[root@VM_15_146_centos ~]# go run main.go
{
        "name": "op_name",
        "desc": "op_desc",
        "modid": 12345,
        "cmdid": 54321,
        "expression": "expression",
        "timeout_ms": 80
}

故障现象

有一天,这样跑了一年的代码突然就不对了,测试发现,OperationQueryCKV结构序列化之后modid和cmdid都消失了,很邪乎。告诉我这个问题后,我也是一脸懵,我最近没有改动相关代码呀。都跑了一年了你跟我说这东西不行了?

故障原因

查看代码提交记录,一处改动进入视野,最近新增了一个查询数据库的功能,与OperationQueryCKV类似的,新增定义了一个OperationQuerySQL,只是新定义了结构,还没有实现和严格测试。

type OperationQuerySQL struct {
   L5   
   User string `json:"user"`
   Pwd  string `json:"pwd"`
}

type Operation struct {
   CommonAttr
   *OperationQueryCKV
   *OperationQuerySQL 
}

查询数据库也需要一个L5,一贯风格,新增一个L5的匿名变量,完活。估计就是两个匿名L5,把值搞没了。试着跑一下样例代码。

func main() {
   op := &Operation{
      CommonAttr{
         Name: "op_name",
         Desc: "op_desc",
      },
      &OperationQueryCKV{
         L5: L5{
            Modid: 12345,
            Cmdid: 54321,
         },
         Expression: "expression",
         TimeoutMs:  80,
      },
      nil,
   }
   b, _ := json.MarshalIndent(op, "", "\t")
   fmt.Printf("%s\n", string(b))
}

结果,我的L5果然没有了。根本原因应该是匿名变量导致的json字段key重复。

{
        "name": "op_name",
        "desc": "op_desc",
        "expression": "expression",
        "timeout_ms": 80
}

解决方法

这样的坑,在分工合作时会不经意引入,有时比较隐晦。

暂时也不知道好的办法,建议是不用或者少用匿名字段,如果需要使用匿名字段,每项都定义不同的`json:”_key_“`。