暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

ClickHouse用户资源隔离在 GrowingIO 的实践

GrowingIO技术团队 2021-11-05
1156

↑ 点击关注 ↑ 更多技术干货


业务场景


对ClickHouse做多租户,是一个非常必要的场景。如果不加限制,用户可能会过度消耗数据库服务的资源,影响其他用户使用。本文尝试在硬件级别的隔离之外,给大家展示另外一种相对便宜的方法。
本文先描述 ClickHouse 提供哪些资源限制的机制,再描述 GrowingIO 的实践。由于用户资源使用限制是用户管理的一种,本文也会加入用户管理的内容。

Hello World


一般通过 systemd 托管 ClickHouse,使用工具渲染模板得出配置文件,由于我们的目的只是演示,所以简化了相关步骤和配置。
在 etc/clickhouse目录创建 2 个文件:
    ── conf
    ├── config.xml
    └── users.xml
    然后在任意目录运行:
      clickhouse server --config=/etc/clickhouse/conf/config.xml
      就让 ClickHouse 按照配置文件运行,同时 ClickHouse 监听配置文件,如有变化,不需要重启就能按新的配置运行。
      The server tracks changes in config files... and reloads the settings for users and clusters on the fly (https://clickhouse.com/docs/en/operations/configuration-files/#implementation-details)
      这是简约版的 config.xml 的配置 demo:
        <?xml version="1.0"?>
        <yandex>
        <logger>
        <level>trace</level>
        <log>/tmp/log/clickhouse-server.log</log>
        <errorlog>/tmp/log/clickhouse-server.err.log</errorlog>
        <size>1000M</size>
        <count>10</count>
        </logger>
        <query_log>
        <database>system</database>
        <table>query_log</table>
        <partition_by>toYYYYMM(event_date)</partition_by>
        <flush_interval_milliseconds>1000</flush_interval_milliseconds>
        </query_log>
        <tcp_port>9000</tcp_port>
        <listen_host>127.0.0.1</listen_host>
        <access_control_path>/tmp/ledzeppelin/</access_control_path>
        <max_concurrent_queries>500</max_concurrent_queries>
        <mark_cache_size>5368709120</mark_cache_size>
        <path>./clickhouse/</path>
        <users_config>users.xml</users_config>
        </yandex>
        注意到 users.xml 绑定了 config.xml 当前目录的 users.xml
          <?xml version="1.0"?>
          <yandex>
          <profiles>
          <default>
          <readonly>1</readonly>
          </default>
          <pA>
          <max_memory_usage>10G</max_memory_usage>
          <max_memory_usage_for_user>10G</max_memory_usage_for_user>
          <max_memory_usage_for_all_queries>10G</max_memory_usage_for_all_queries>
          <max_query_size>1073741824</max_query_size>
          <readonly>1</readonly>
          </pA>
          </profiles>
          <users>
          <zeppelin>
          <!-- <password_sha256_hex></password_sha256_hex> -->
          <password>password</password>
          <networks>
          <ip>::/0</ip>
          </networks>
          <profile>pA</profile>
          <quota>qA</quota>
          </zeppelin>
          </users>
          <quotas>
          <qA>
          <interval>
          <!-- 以秒为单位 -->
          <duration>10</duration>
          <!-- 10 秒内只能查询一次 -->
          <queries>2</queries>
          <errors>0</errors>
          <result_rows>0</result_rows>
          <read_rows>0</read_rows>
          <execution_time>0</execution_time>
          </interval>
          </qA>
          </quotas>
          </yandex>
          可以初步看到 user, profile, quotas 之间的引用关系。这两个配置文件规定了用户 zeppelin 只能 10 秒查询 2 次
          设置 Profile pA 为只读: 1, 用户 zeppelin 引用了 pA, 当 zeppelin 执行写入语句的时候,会报错:

          Quotas 和 Profile


          Quotas 和 Settings Profiles 是 ClickHouse 用来控制用户资源的两种方式。
          • Quota
          • Profile
          从第 2 节我们可以获取这两种限制机制的初步印象。它们在 ClickHouse 的文档中的定义分别是:
          Quotas allow you to limit resource usage over a period of time or track the use of resources.
          A settings profile is a collection of settings grouped under the same name.
          设置 (settings) 也是一种限制。Profile 的定义并未突出”限制“的意思,但我们完全可以把它当作限制来用。
          上图的 Profile demo 规定了用户”能读的行数“,max_rows_to_read,问题在于,这是一次能读的最大行数,还是用户存活的时候,总共能读的行数限制?答案是执行一次查询的时候,数据库管理工具能查询的最大行数。
          A maximum number of rows that can be read from a table when running a query. 
          (https://clickhouse.com/docs/en/operations/settings/query-complexity/#max-rows-to-read)
          可以看出Profile 和 Quotas 的本质区别是 Profile 的设置与时间无关,Profile 针对每一次查询,以最容易理解的 readonly 为例,”只读“不针对 10 秒 或 1 小时的用户行为,它针对的是每一次查询,对于每一次查询而言,Profile 的限制是实实在在的,而不像 Quota,这一秒还有 10 个配额,下一秒可能没有配额了,导致查询被丢弃。
          为了进一步理解为什么要将 Quota 和 Interval 联系在一起,我们需要联想到 QPS(Queries per second):
          如图,当流量非常迅猛的时候,它会每个 Interval 的前期就把配额全部耗尽,导致剩下的时间内无配额可用,进而导致断流,比如 A, B 区间。当流量相对缓和的时候,它在整个时间区间的分布会比较均匀,比如 C 区间。
          QPS 和 TIME 的积分就等于 Quota(配额)。
          可以看出,ClickHouse 的 Quota 和 token bucket 限流方法有许多相似之处。令牌桶在每个时间段 (Interval) 生成一定的配额,当配额耗尽,新的查询就要被丢弃。
          从源码中可以看出,每次查询的时候,都去检查(checkExceeded())是否超过配额
          每个 interval 都有多种资源(resource_type), 比如 1是一种 type, 检查最大库存 max,检查已经使用的配额 used, 如果 used > max, 则报错。

          SQL 驱动的访问控制


          We recommend using SQL-driven workflow. Both of the configuration methods work simultaneously, so if you use the server configuration files for managing accounts and access rights, you can smoothly switch to SQL-driven workflow. (https://clickhouse.com/docs/en/operations/access-rights/)

          ClickHouse 推荐使用 SQL 驱动的访问控制方式。第 2 节展示了”Server configuration files users.xml and config.xml“ 的控制方式,对于不复杂的场景而言,已经够用。配置文件的方式涉及到安全问题,并且在增加用户的时候存在不灵活的问题,包括管理静态用户,动态(临时)用户。现在我们将通过一系列的实际操作验证SQL驱动方式的可行性。

          4.0 初步解决方案
          1. 通过 default用户创建超级管理员 ledzeppelin 以及用户 zeppelin。(如果在 users.xml 定义了 zeppelin, 之后就无法更改, 报错: Cannot update user zeppelin in users.xml because this storage is readonly)。
          2. 解除 default 的权限。
          3. 以超级管理员的身份, 通过 SQL 动态配置 zeppelin 资源
          如图,普通用户 zeppelin 没有任何权限,它是被动的,不能执行任何创建 ”ClickHouse access entities“ 的操作;而 ledzeppelin 作为超级管理员则是主动的,它组装了 Profile, Quota, Role 等访问实体,最后将角色赋予普通用户 zeppelin,重新定义 zeppelin 的属性,达到用户管理的目的。我们也在这次实践中,从资源限制出发,最后扩展到更普遍的用户管理。
          4.1 创建 zeppelin 用户
          按照 ClickHouse 的推荐方式创建用户
          4.1.1 给 default 用户 access_management 权限
            <user>
            <default>
            <!-- 其他配置 -->
            <access_management>1</access_management>
            </default>
            </user>
            4.1.2 用 default 用户创建超级管理员 ledzeppelin 及普通用户 zeppelin
              CREATE USER ledzeppelin;
              GRANT ALL ON *.* TO ledzeppelin WITH GRANT OPTION;
              CREATE USER zeppelin;
              实践中必须设置密码, 比如
                CREATE USER MJ IDENTIFIED WITH sha256_password BY 'qwerty';
                登录:
                  clickhouse-client -u MJ --password qwerty
                  考虑到隐私, 可以将密码设置为环境变量
                    clickhouse-client -u MJ --password ${CLICKHOUSE_MJ_PASSWORD}
                    4.1.3 关闭 default 用户的 SQL 访问控制权限
                    出于对安全的考虑, 我们需要关掉 default 的权限避免出现制造另外一个超级管理员等隐患
                    首先退出 default 的登录, 注意不能通过 SQL 的方式更改 default 用户的属性, 不然会报错:
                      REVOKE ALL ON *.* FROM default


                      Query id: 7ab4a62a-62d1-4d0b-bbfe-c27d0e595f65




                      0 rows in set. Elapsed: 0.002 sec.


                      Received exception from server (version 21.10.2):
                      Code: 495. DB::Exception: Received from localhost:9000. DB::Exception: Cannot update user `default` in users.xml because this storage is readonly: Couldn't update user `default`. Successfully updated: none. (ACCESS_STORAGE_READONLY)
                      我们只能通过修改 XML 的方式改 default 的配置
                      按 ClickHouse 的方式:
                      1. 使 default 用户只读, 在 users.xml 设置一个只读 Profile
                        <profiles>
                        <!-- Profile that allows only read queries. -->
                        <readonly_profile>
                        <readonly>2</readonly>
                        <!-- 其他配置 -->
                        </readonly_profile>
                        </profiles>
                        更改 default 用户的 Profile:
                          <users>
                          <profile> readonly_profile</profile>
                          <!-- 其他配置 -->
                          </users>
                          并删掉这一行:
                            <access_management>1</access_management>
                            ClickHouse 会自动加载配置, 现在只剩下一个超级管理员: ledzeppelin
                            4.2 通过 SQL 动态管理资源
                            4.2.1 创建一个 Profile
                            以readonly
                            (https://clickhouse.com/docs/en/operations/settings/permissions-for-queries/#settings_readonly) 为例, 是因为这个属性容易测试, 0 的时候可以创建数据库, 1 和 2 的时候不能.
                              CREATE SETTINGS PROFILE IF NOT EXISTS z_profile
                              SETTINGS
                              readonly = 2
                              READONLY
                              4.2.2 创建一个 Quota
                              10 秒内只能查询一次
                                CREATE QUOTA IF NOT EXISTS z_quota
                                FOR INTERVAL 10 second
                                MAX queries 1
                                4.2.3 创建 Role
                                  CREATE ROLE IF NOT EXISTS z_role
                                  4.2.3.1 赋予 Role 权限
                                    GRANT SELECT ON db.* TO z_role;
                                    允许 z_role 访问 db 数据库,注意到,我们并不是赋予某个用户权限,而是赋予角色 (Role) 权限,而这一角色可以赋予其他用户。通过这种方式,我们在权限和用户之间建立了一个抽象层。
                                    4.2.4 创建一个垃圾回收 Role
                                      CREATE ROLE IF NOT EXISTS gc_role
                                      作用是用来绑定不用的 profiles, quotas
                                      4.2.5 Role 绑定 Profile, Quota
                                      4.2.6 应用到用户 zeppelin
                                      查看 zeppelin 有绑定了哪些 role
                                        ALTER SETTINGS PROFILE z_profile TO z_role
                                        ALTER QUOTA z_quota TO z_role;
                                        如果有, 假设是 old_role, 就先解绑
                                          SELECT * FROM system.role_grants WHERE user_name LIKE 'zeppelin'
                                          绑定新的角色:
                                            GRANT z_role TO zeppelin
                                            4.2.7 必须 REVOKE(重要)
                                            我们的设想是, profile 和 quota 绑定 role, role 绑定用户, 如果需要更改用户的配置, 有两种方法
                                            1. 换一个 role
                                            2. 改当前 role 的 profile/quota
                                            第二种方法有问题, 因为改了 profile 之后, 并不会直接刷新用户的配置(可能是 ClickHouse 的 bug, 我们还在验证中), 这时候就需要将 用户的role  revoke 下来, 再挂上去
                                            保险起见, 应该按照以下范式操作:
                                            1. `revoke z_role from zeppelin`
                                            2. 修改 z_role 下的 profile 或 quota
                                            3. `grant z_role from zeppelin`
                                            或:
                                            1. 修改 z_role 下的 profile 或 quota
                                            2. `revoke z_role from zeppelin`
                                            3. `grant z_role from zeppelin`
                                            4.2.8 绑定一个新的 Quota
                                            改为 5 秒一次查询
                                              CREATE QUOTA IF NOT EXISTS z_quota_5
                                              FOR INTERVAL 5 second
                                              MAX queries 1
                                              将原来的 z_quota 绑定到 gc_role
                                                ALTER QUOTA z_quota TO gc_role;
                                                绑上新 quota:
                                                  ALTER QUOTA z_quota_5 TO z_role;
                                                  刷新:
                                                    revoke z_role from zeppelin;
                                                    grant z_role to zeppelin;
                                                    检查:
                                                      SELECT
                                                      name,
                                                      apply_to_list
                                                      FROM system.quotas
                                                      WHERE name LIKE 'z_quota_5'


                                                      Query id: dc5b2ece-c792-4585-95d8-8b4275631664


                                                      ┌─name──────┬─apply_to_list─┐
                                                      │ z_quota_5 │ ['z_role'] │
                                                      └───────────┴───────────────┘


                                                      1 rows in set. Elapsed: 0.004 sec.
                                                      4.2.9 绑定一个新的 Profile
                                                        CREATE SETTINGS PROFILE IF NOT EXISTS z_profile_permission_type_0
                                                        SETTINGS
                                                        readonly = 0
                                                        READONLY
                                                        更改 profile 的绑定对象, 先把 z_profile 绑定到 gc_role
                                                          ALTER SETTINGS PROFILE z_profile TO gc_role
                                                          将新的 z_profile_permission_type_0 绑到 z_role
                                                            ALTER SETTINGS PROFILE z_profile_permission_type_0 TO z_role
                                                            刷新配置:
                                                              revoke z_role from zeppelin;
                                                              grant z_role to zeppelin;
                                                                SELECT
                                                                name,
                                                                apply_to_list
                                                                FROM system.settings_profiles


                                                                Query id: 51d1c2de-ced7-4558-a164-020a76a53d97


                                                                ┌─name────────────────────────┬─apply_to_list─┐
                                                                │ readonly │ [] │
                                                                default │ [] │
                                                                │ z_profile │ ['gc_role'] │
                                                                │ z_profile_permission_type_0 │ ['z_role'] │
                                                                └─────────────────────────────┴───────────────┘

                                                                5.SQL驱动设置持久化


                                                                本地存储/本地持久化,和数据目录的path一样。根据 
                                                                https://clickhouse.com/docs/en/operations/server-configuration-parameters/settings/#access_control_path ,通过 SQL 创建的属性存储在 access_control_path, 默认值为 var/lib/clickhouse/access/
                                                                Path to a folder where a ClickHouse server stores user and role configurations created by SQL commands.

                                                                设置位置: config.xml

                                                                  <yandex>
                                                                  ...
                                                                  <access_control_path>/var/lib/clickhouse/access/ledzeppelin/</access_control_path>
                                                                  ...
                                                                  </yandex
                                                                  查看目录:
                                                                    ╭─/var/lib/clickhouse/access/ledzeppelin  
                                                                    ╰─➤ ls
                                                                    094e5b76-1b4d-97a5-43cd-bbb3ccceb688.sql 9c492851-bf60-7753-7c14-c05f4be46b8e.sql
                                                                    2949a246-848f-fc07-f1b9-a3f7246b31a0.sql f340165d-0ec4-f90e-fb55-74c8d0cfd7ae.sql
                                                                    394e47fd-b701-d017-6857-0d8744d830b1.sql quotas.list
                                                                    3d44c8a1-01b3-6f68-eff7-1e20a9080296.sql roles.list
                                                                    3f447b54-b515-aa93-bacc-1a649aeac19c.sql row_policies.list
                                                                    674912da-5593-e497-c5d8-d058e916c7a5.sql settings_profiles.list
                                                                    6c44ad5c-4354-c154-048e-8e293ff427ab.sql users.list
                                                                    另外, 注意到重启 ClickHouse 之后, ClickHouse 从 access_control_path 恢复配置:
                                                                    如果删掉这些文件,那么重启 ClickHouse 之后,之前通过 SQL 创建的"访问实体"(ClickHouse Access Entities) 就会消失。

                                                                    结论


                                                                    本文从 ClickHouse 用户资源隔离开始,扩展到用户 ClickHouse 用户管理。我们通过 Quotas 和 Profile 实现了资源隔离的业务;在用户管理中,我们使用了角色 (Role) ,创建了一个新的抽象层,这样一来,我们就不需要在管理用户的时候直接操作权限,而是将角色作为一种组件赋予用户。角色也完全可以封装资源。我们可以通过配置文件或者 SQL 的方式部署这些访问实体,后者是 ClickHouse 倡导的方式。最后,本文只涉及 ClickHouse 用户管理的冰山一角,希望抛砖引玉,对大家有所帮助。

                                                                    参考


                                                                    • https://altinity.com/blog/goodbye-xml-hello-sql-clickhouse-user-management-goes-pro

                                                                    • https://clickhouse.com/docs/en/operations/access-rights/ 

                                                                    • Quotas:https://clickhouse.com/docs/en/operations/quotas/

                                                                    • Settings Profiles:

                                                                      https://clickhouse.com/docs/en/operations/settings/settings-profiles/

                                                                    • ClickHouse 的推荐方式:

                                                                      https://clickhouse.com/docs/en/operations/access-rights/#enabling-access-control


                                                                    关于 GrowingIO


                                                                    作为国内领先的一站式数据增长引擎整体方案服务商,GrowingIO以数据智能分析为核心,通过构建客户数据平台,打造增长营销闭环,帮助企业提升数据驱动能力,赋能商业决策、实现业务增长。

                                                                    GrowingIO专注于零售、电商、保险、酒旅航司、教育、内容社区等行业,成立以来,累计服务超过1500家企业级客户,获得LVMH集团、百事、达能、老佛爷百货、戴尔、lululemon、美素佳儿、宜家、乐高、美的、海尔、安踏、汉光百货、上汽集团、广汽蔚来、理想汽车、招商仁和人寿、飞鹤、红星美凯龙、东方航空、滴滴、新东方、喜茶、每日优鲜、奈雪的茶、永辉超市等客户的青睐。


                                                                    招聘信息


                                                                    GrowingIO技术团队是一个活力四射、对技术充满激情的团队,多个岗位持续招聘中!诚招前端工程师/大数据工程师/Java工程师等,欢迎有兴趣的同学投递简历至:jianli@growingio.com(邮件标题请注明具体岗位名称),更多职位及信息可进入招聘官网查看。


                                                                    点击「阅读原文」获取 GrowingIO 15 天免费试用!

                                                                    ↓↓↓


                                                                    文章转载自GrowingIO技术团队,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                                                                    评论