• etcd备忘录


    etcd简介:

    etc是强一致性,高可用的分布式key-value存储系统;主要存储一些分布式系统或集群 使用的数据;

    特点:

    • 高可用:多etcd节点组成集群
    • go语言编写且开源
    • raft算法实现 强一致性(C):其算法的选主策略(P),日志复制(C) 保证 多数从节点都同步最新数据后才返回给client; 保证实现CAP原理中的CP; 无法保证A(可用性无法保证,因为网络分区导致数据同步时间不确定)
    • key有TTL属性(lease租约):设置key的生命周期
    • 支持事务操作
    • 支持key的watch功能:当检测到key的值变化时,通知监测的服务
    • 客户端与etcd通过grpc及http2.0协议通信:保证多路复用,减少tcp连接数,提高网络通信效率;
    • 用户可以通过 命令行/grpc/http restful api(内部转为grpc) 3种方式操作etcd;
    • 默认存储2G数据,最多支持8G,再多就会告警;
    • etcd集群 在使用 http restful api方式通信时,可以通过 etcd-gateway方式代理 集群中所有节点, 通过访问网关地址来访问etcd集群,使客户端不用关系每个节点的ip变化;
    • etcd集群 在使用 grpc方式通信时,可以通过 grpc-proxy方式 代理 集群中所有节点, 通过访问代理地址来访问etcd集群,使客户端不用关系每个节点的ip变化;

    使用场景:

    • 分布式锁功能:如 多个相同子系统 接口防并发
    • 配置管理:多个业务服务从etcd中获取key对应的value(配置的值)来启动服务;可以通过etcd的watch功能,当配置变化时,对应的业务服务可以实时修改配置,无需重启服务;
    • 服务注册/发现: 通过将微服务的ip/port等信息同步到etcd中,其他服务需要时从etcd找到对应的微服务信息; 但是需要考虑 服务的监控问题;

    etcd分布式锁功能实现原理: 

    互斥机制:多个客户端同时尝试获取同一个锁时(同样的key),只有 revision(全局递增)最小的key会获取锁成功;  

    防止死锁机制:通过 lease(租约) 通过设置ttl(类似redis的超时时间)来实现key的存活时间,到点自动删除; 保证其他服务一定能在固定时间后获取锁;

    watch机制:能在key被删除时,通知其他客户端成功获取锁;

    etcd分布式锁python版实现代码

     通过etcd自带的分布式锁实现 接口级别的分布式互斥锁的装饰器实现

    1. import time
    2. from functools import wraps
    3. # pip install etcd3
    4. import etcd3
    5. from app import fail_response
    6. from app.utils import logger, ApiException
    7. etcd = etcd3.client("127.0.0.1")
    8. def synchronized_api_by_etcd(name=None, expire=None):
    9. """ 通过etcd自带的分布式锁实现 接口级别的分布式互斥锁的装饰器实现
    10. 注意事项: 只适用于接口级别
    11. :param name: 锁名称 保证唯一性
    12. Usage:
    13. >>> @api.resource('/test/api')
    14. >>> class TestApiController(BaseController):
    15. >>> '''测试功能
    16. >>> '''
    17. >>>
    18. >>> @synchronized_api_by_etcd(name="dopamine:SYNC:STOCK:MODIFY", expire=3):
    19. >>> def post(self):
    20. >>> pass
    21. """
    22. def decorator(func):
    23. @wraps(func)
    24. def wrapper(*args, **kwargs):
    25. """ 对原始函数进行操作
    26. """
    27. # 重试2次
    28. retry_count = 2
    29. while retry_count:
    30. logger.debug(f"开始尝试对任务'{name}'进行加锁")
    31. try:
    32. # 实现上锁,且设置KEY的TTL为 入参的expire秒
    33. # 实现退出with语句时 删除此锁
    34. with etcd.lock(f"{name}", expire) as lock:
    35. # 获取锁失败,此锁可能被其他程序获取;
    36. if not lock.is_acquired():
    37. logger.debug("获取锁失败,重新尝试")
    38. time.sleep(0.5)
    39. retry_count -= 1
    40. try:
    41. logger.debug(f"任务'{name}'加锁成功,开始执行业务逻辑")
    42. result = func(*args, **kwargs)
    43. except ApiException as e:
    44. result = fail_response(e.code, e.message)
    45. except Exception:
    46. logger.error(f"任务'{name}'执行失败,发生未知异常", exc_info=True)
    47. result = fail_response()
    48. logger.debug(f"任务'{name}'运行完毕,开始释放锁")
    49. return result
    50. except Exception as e:
    51. # 加锁失败,出现异常情况
    52. print(e)
    53. time.sleep(0.5)
    54. retry_count -= 1
    55. logger.debug(f"当前任务'{name}'尝试加锁失败,本任务已在其它位置运行")
    56. return fail_response(500, "无法并发执行")
    57. return wrapper
    58. return decorator

    etcd分布式锁go版实现代码

    在gin的中间件中实现 接口级分布式锁

    1. //
    2. // @Description: 接口级别的 分布式互斥锁;实现同一时间只能有一个服务提供访问,防并发操作
    3. // @param name: 锁的名字
    4. // @return gin.HandlerFunc:
    5. //
    6. func SynchronizedApi(name string) gin.HandlerFunc {
    7. var l *concurrency.Mutex
    8. synchronizedApiByEtcdLock := func(name string) {
    9. //新建一个lease(租约)
    10. //注意 此租约会一直刷新超时时间(自动续约),保证未手动关闭租约时此租约一直有效,此方式能保证 在后面的获取锁,及释放锁 时间段内 锁能一直有效,最大化防止并发错误;
    11. //所以 不需要用户设置过期时间,只需要写死1s保证最小超时时间即可;这样即使 程序异常退出,1s后可以获取锁,最小化减少锁的影响时间;
    12. //租约退出条件为:直到 手动关闭租约或程序异常退出,租约里的锁会在声明的expire时间后被删除,保证不会死锁;
    13. session, err := concurrency.NewSession(etcd.EtcdCli, concurrency.WithTTL(1))
    14. if err != nil {
    15. glog.Log.Error("获取lease(租约)失败")
    16. panic(err)
    17. }
    18. //最后关闭lease
    19. defer func() { _ = session.Close() }()
    20. //新建一个锁对象
    21. l = concurrency.NewMutex(session, name)
    22. //设置100毫秒后不再尝试获取锁
    23. c1 := context.Background()
    24. c2, cancel := context.WithTimeout(c1, 100*time.Millisecond)
    25. defer cancel()
    26. //尝试上锁
    27. err = l.Lock(c2)
    28. if err != nil {
    29. glog.Log.Info("上锁失败")
    30. panic(api_error.New(504, string("接口不能并发请求")))
    31. }
    32. glog.Log.Info("上锁成功")
    33. time.Sleep(5*time.Second)
    34. }
    35. synchronizedApiByEtcdUnLock := func() {
    36. //开始释放锁
    37. err := l.Unlock(context.TODO())
    38. if err != nil {
    39. glog.Log.Info("删除锁对应的key失败,后续etcd应会自动ttl删除")
    40. panic(err)
    41. }
    42. glog.Log.Info("释放锁成功")
    43. }
    44. return func(c *gin.Context) {
    45. //上锁
    46. synchronizedApiByEtcdLock(name)
    47. //继续执行请求的后续代码
    48. c.Next()
    49. //释放锁
    50. synchronizedApiByEtcdUnLock()
    51. }
    52. }

    调用方法

    1. // 路由注册地方调用
    2. apiGroup.POST("/test", SynchronizedApi("projectName/groupName/test"), OtherHandler)

    注意:python版无法实现像go版的自动续约功能,只能手动调用  Lock.refresh() 续约;

    相关链接:

    CentOS安装Etcd_当年的春天的博客-CSDN博客_centos etcd

    ETCD客户端IDE

    python语言etcd3的客户端包

    CAP原理在ETCD中的应用

  • 相关阅读:
    android FileOutputStream 写入文件,但是文件大小为空
    《前端运维》四、Jenkins--持续构建
    基于JSP+MySQL+HTML5的旅游网站系统
    快速创建django项目管理系统流程
    正则表达式详细知识点
    eNSP简单配置命令以及DHCP
    什么是容错性(Fault Tolerance)?解释容错性的实现方法
    8.(Python数模)(预测模型一)马尔科夫链预测
    开学季征文 | 新学期,新规划
    html+css 热茶效果
  • 原文地址:https://blog.csdn.net/rgc_520_zyl/article/details/126466645