Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

用fbp重构和面向性能优化 #2

Open
dannyvi opened this issue Dec 6, 2020 · 0 comments
Open

用fbp重构和面向性能优化 #2

dannyvi opened this issue Dec 6, 2020 · 0 comments

Comments

@dannyvi
Copy link

dannyvi commented Dec 6, 2020

代码审计和提案

1 原理

一次后端调用从端口经过网络服务到后端处理程序到返回响应结果,完成服务。目前django框架通过在部署过程中采用轻量级异步协议的方式实现了类异步服务,比较好的解决并发性能方面的问题。

通常一个后端服务至少有一个以上的持久层。无论web框架采用何种架构,最终存储会通过持久层进入数据库。由于数据库的io特性和事务特性,需要采用队列方式将数据排队存取,并且保持对所有连接的一致性。持久化方案往往成为性能重点瓶颈和设计的焦点。

这里我们的优化方案主要基于测量接口调用的时间和io关键指标,即以调用一个接口的完整周期和数据库因素作为测度标准,来衡量我们应该采用何种技术和方案,解决性能问题,和衡量我们大概能在什么样的程度上解决问题。

另外我们会基于可复用性来分析微服务方案。

2 测试技术

操作计数

对一次调用中的数据库操作进行计数可以简单明了的说明这次调用进行了多少次数据库操作。通常我们希望操作越少越好。其它涉及io操作的代码也可以采用这种方式,不过这里我们主要就是计量数据库。

profile

gdb为开发者提供调试和性能工具,获取代码在运行时的度量和截面状态, python 同样提供了pdb, cProfile, pstats 等系列统计和调试工具。通过中间件技术和配置,我们可以在框架中插入profiling功能,来为开发提供更加精确的参考。

集成测试(Integration Test)

对接口做集成测试,通过一些自动化脚本调用每个接口,跟profile结合,可以获得服务的整体性能。

3 组织结构

完全同构

在业务升级过程中必要保持向下兼容和依赖性时,通常会采用完全同构方法,对代码结构和数据表保持一致,在代码层优化或者重新构成。

django orm 数据表关系和用法

class UserPerm(OwnerPerm):
    owner = models.ForeignKey('oneid_meta.User', on_delete=models.CASCADE)

这条语句作为外键,给UserPerm创建一个owner属性映射,同时会为User对象创建一个默认反向引用。

u = User.objects.get(pk=pk)
u.userperm_set.all()

可以省掉这样的写法。

    @property
    def perms(self):
        '''
        所有权限
        '''
        return UserPerm.valid_objects.filter(owner=self)

也可以在ForeignKey里显式给予命名。

class UserPerm(OwnerPerm):
    owner = models.ForeignKey('oneid_meta.User', related_name='perms', on_delete=models.CASCADE)

...
# 可以通过related_name 获取对反向关系的引用。
u = User.objects.get(pk=pk)
perms = u.perms.all()
...

每一个引用Field(OneToOne, ForeignKey, ManyToMany) 都有一个相应的反引用字段。

相同接口

保持相同接口通常也是一种,成本较小的方案,这种方案无需重写前端。

异构重构

如果业务目标调整或者重新升级业务,会从流程上重新梳理,产生新的架构。

代码复用和微服务

微服务在思想上被认为是先进的。它为我们提供了可复用性和灵活性。

arkid数据表基本运作

在arkid中引用关系最多的两张表 UserSite
User 表和它关联的权限组是arkid 服务的中心。绝大部分的查询和更改都集中在这几个表的条目。
整体而言,这几个表关系要表达的关系是用户有分组属性和部门属性,这两个属性连同用户本身都有一系列权限,这些权限是可以被继承到用户的。用户权限和其所属的组以及部门权限决定了对于应用和配置的作用。所以,用户、分组、部门和权限的高效查询和分配作为一个核心,是整个server的起点,可以作为一个基础微服务进行考虑。

DeptMember 和 GroupMember 作为多对多关系的一种显式建表方式,这里予以省略。

User -> UserPerm

-> Group -> GroupPerm
-> Dept -> DeptPerm

为了达到高效查询目的,实际上这里应当简化成4张表。将UserPerm, GroupPerm, DeptPerm 统一压缩到Perm表

User -> Perm

-> Group -> Perm
-> Dept -> Perm

class Perm(models.Model):
    name = models.CharField()

class Group(models.Model):
    label = models.CharField()
    group_perms = models.ManyToManyField(Perm, related_name='groups')

class Dept(models.Model):
    label = models.CharField()
    dept_perms = models.ManyToManyField(Perm, related_name='depts')

class User(models.Model):
    user_perms = models.ManyToManyField(Perm, related_name='users')
    groups = models.ManyToManyField(Group, related_name='users')
    depts = models.ManyToManyField(Dept, related_name='users')

    @property
    def all_perms(self):
        query = Q(users__in=[self]) | Q(groups__in=self.groups.all()) | Q(depts__in=self.depts.all())
        return Perm.objects.filter(query)   

系统原有设计依然不失为一种可选方案,当多对多关系存储量较大时,创建中间表可以避免读取基础数据时额外查询外键的开销。

class Perm(models.Model):
    name = models.CharField()

class Group(models.Model):
   label = models.CharField()

class User(models.Model):
    username = models.CharField()
    
    @property
    def all_perms(self):
        query = Q(user_relation__user=self) | Q(group_relation__group_id__in=self.group_relation.values_list('group', flat=True))
        return Perm.objects.filter(query)

class UserGroup(models.Model):
    user = models.ForeignKey(User, related_name='group_relation')
    group = models.ForeignKey(Group, related_name='user_relation')


class UserPerm(models.Model):
    perm = models.ForeignKey(Perm, related_name='user_relation')
    user = models.ForeignKey(User, related_name='perm_relation')

class GroupPerm(models.Model):
    perm = models.ForeignKey(Perm, related_name='group_relation')
    group = models.ForeignKey(Group, related_name='perm_relation')

可以一次性获取该用户所有权限,分组重新分配以后,结果也会跟着变化。实际上,在数据库查询中进行函数式编程考量依然是重要的。从查询的角度消除副作用,关注输入和输出的一致性将会显著的影响服务性能。

表字段、查询关系和应用算法设计

对于下面的特性,也要设法将其设计到表字段中,让权限可以通过一次组合查询获取到全部结果。配合数据库的缓存机制,可以做到非常高效。在更新表的时候增加字段复杂性和维护字段统一性的难度增加开销很小。在每次查询获取结果的过程当中迭代,开销将会大到难以忍受。

        (1, '所有人可见'),
        (2, '节点成员可见'),
        (3, '节点成员及其下属节点均可见'),
        (4, '只对指定人、节点可见'),
        (5, '所有人不可见'),

arkid 数据表关系概要

其他微服务按照具体应用分解为:

表依赖 Model\Prefix Github Ding Wechat Alipay Minio QQ
Site {pref}Config
{pref}App
User {pref}User
微服务化之后的主要app列表
  • oneid (系统)
  • base (基本用户和权限)
  • oauth2_provider
  • github
  • qq
  • wechat
  • alipay
  • ding
    ...
    后面的子app大致都共同依赖于 base。除此以外,各app之间相互基本独立,可装卸。

每个应用大致的构成是相同的: models, serializer, views, urls, 以及相关的utils 和 tasks。当我们分配好了应用的全部要素以后。每个子app 可以被挂载或者卸载。

4 方法

通常后端service在开发早期的性能问题是由于查询代码性能导致的,这时一般是单库单server。通过db优化和流程优化和代码优化可以将单服务器和数据库性能优化到最佳性能。性能合适的数据库和配置可以帮助服务支撑用户和数据快速增长。随着用户增长到服务性能极限,可以通过提升配置来大幅提升服务的响应能力。现在云服务一般会给应用商提供简化的saas,通过一些简单架构方法可以提供进一步性能支撑。这里我们聚焦于早期优化。

基于查询计数

重复操作和不当操作是优化的首要目标。

批量操作

案例:

        for plugin_path in plugin_paths:
            CrontabPlugin.valid_objects.create(
                name=plugin_path,
                import_path=plugin_path,
                is_active=False,
            )

这里的核心优化点在创建语句。假设plugin_paths有10个元素,create语句将导致10次写数据库。io过多将导致数据库锁死。

  1. 将单条创建转化成批量创建语句,显著降低io。
  2. 将循环转换成列表处理,可以少许改进速度。

解决方案:

plugins = [CrontabPlugin(name=plugin_path, import_path=plugin_path, is_active=False) for plugin_path in plugin_paths ]
queryset = CrontabPlugin.valid_objects.bulk_create(plugins)

通过bulk_create将写数据库从10次减少为1次。
由于python的动态语言特性,使用for语句时每次迭代会动态创建一个Pyobject,列表语句中的迭代是内嵌在cpython里面的,所以效率会高。

案例:

def check_perm_exists():
    '''
    为每个对象:user,gruop,dept创建perm判定结果
    '''
    for perm in Perm.valid_objects.all():
        for user in User.valid_objects.all():
            if not UserPerm.valid_objects.filter(owner=user, perm=perm).exists():
                UserPerm.valid_objects.create(
                    owner=user,
                    perm=perm,
                    dept_perm_value=False,
                    group_perm_value=False,
                    status=0,
                    value=perm.default_value,
                )
        for group in Group.valid_objects.all():
            if not GroupPerm.valid_objects.filter(owner=group, perm=perm).exists():
                GroupPerm.valid_objects.create(
                    owner=group,
                    perm=perm,
                    status=0,
                    value=perm.default_value,
                )
        for dept in Dept.valid_objects.all():
            if not DeptPerm.valid_objects.filter(owner=dept, perm=perm).exists():
                DeptPerm.valid_objects.create(
                    owner=dept,
                    perm=perm,
                    status=0,
                    value=perm.default_value,
                )

这里涉及到Perm(权限),User(用户),UserPerm(用户权限),Dept(部门),DeptPerm(部门权限),Group(分组), GroupPerm(分组权限)7张表的查询和更新。
从同构的角度来进行优化的话,应设法优化掉两次迭代带来的 O _n2 复杂度的数据库写操作。

由于unique_together属性存在,实际上我们并不需要逐个检测是否存在,可以批量创建。这里valid_manager 会过滤掉is_del=False的条目,所以不应采用。
解决方案:

perms = Perm.objects.values('id', 'default_value')

users = User.objects.values_list('id', flat=True)
groups = Group.objects.values_list('id', flat=True)
departments = Dept.objects.values_list('id', flat=True)

user_perms = [ 
    UserPerm(owner_id=user, perm_id=perm['id'], status=0, value=perm['default_value'])
    for user in users for perm in perms
]
UserPerm.objects.bulk_create(user_perms,  ignore_conflicts=True)
...

这样我们可以将流程优化为七次数据库操作。

这里通常权限,分组和部门是常量级的表,所以一次全量查询带来的开销是有限的,可以看作是分页级别的开销。
考虑到用户的线性增长,假设存在10种权限。当用户增长到1000时。一次User查询将返回1000条数据,UserPerm写操作会产生10,000条写入。这里的效率依然是不可接受的(不过作为定时任务,避开波峰时期进行这样的操作,也可以说得过去)。严谨而论,有必要在流程上进一步分析,进行某种更加优化的设计。忽略无需更新的条目,或者以空间来换取时间,将全量操作优化为查询和少量更新,达到高效操作的目的。

基于流程

通用操作拆分

注意到LOG_CLI在server中的广泛存在,目前用户审计和操作日志在系统中扮演着重要的角色。对于权限检验,操作日志这样多个接口中存在的低耦合非事务性代码,可以通过拆分的方式将其进行改造,从而在整体上使server性能获得良好的提升。

  1. 分表分库。 作为一个可选项,日志模块可以设置为单独的数据库。
class RequestAccessLog(models.Model):
    '''
    请求基本信息
    '''
    ip = models.CharField(max_length=64, blank=True, null=True, default='', verbose_name='IP地址')
    agent = models.CharField(max_length=512, blank=True, null=True, default='', verbose_name='HTTP_USER_AGENT')
    url = models.TextField(blank=True, null=True, default='', verbose_name='完整请求路径')
    method = models.CharField(max_length=16, blank=True, null=True, default='', verbose_name='REQUEST_METHOD')

class RequestDataClientLog(models.Model):
    '''
    请求内容,定期删除
    '''
    content = models.TextField(blank=True, null=True, default='', verbose_name='请求内容')
    content_type = models.TextField(blank=True, null=True, default='', verbose_name='内容类型')

class Log(models.Model):
    '''
    操作日志,会呈现给用户(管理员) 永久保存
    '''
    uuid = models.UUIDField(default=uuid.uuid4, editable=True, unique=True)
    created = models.DateTimeField(auto_now_add=True, blank=True, null=True, verbose_name='创建时间')
    user_pk = models.IntegerField( blank=True, null=True, verbose_name='操作者')
    username = models.CharField(max_length=128, blank=True, null=True, verbose_name='操作者名称')
    subject = models.CharField(max_length=128, default='', blank=False, null=False, verbose_name='类型')
    summary = models.CharField(max_length=512, default='', blank=False, null=False, verbose_name='事件信息')
    access = models.ForeignKey(RequestAccessLog, blank=True, null=True, on_delete=models.SET_NULL)
    data = models.ForeignKey(RequestDataClientLog, blank=True, null=True, on_delete=models.SET_NULL)

解除外键不仅可以使代码具有更好的独立性和可隔离性,还可以避免在orm中由于引用外键属性带来额外查询问题。

  1. 将日志异步化。
    tasksapp.tasks.py 定义
@shared_task
def log(self, subject, summary, user_pk, username):
    Log.objects.create(    # pylint: disable=no-member
            user_pk=user_pk,
            username=username,
            subject=subject,
            summary=summary,
            access=self.access_log,
            data=self.data_log,
        )

RDBLogExecuter

class RDBLogExecuter(Executer):
    ...
    def create_user(self, user_info):
        '''
        创建用户
        :param dict user_info:
        '''
        subject = 'user_create'
        summary = f'{self.cli.user.log_name} 创建新用户 {self.cli.res.log_name}'

        if self.cli.request:
            url_name = resolve(self.cli.request.path_info).url_name
            if url_name == 'user_register':
                subject = 'ucenter_register'
                summary = f'用户注册: {self.cli.user.log_name}'
        user_pk = self.cli.user.id
        username = self.cli.user.username
        log.apply_async(subject, summary, user_pk, username)

实际执行代码

    def perform_create(self, serializer):
        user = serializer.save()
        LOG_CLI(serializer.instance).create_user()
        return user

我们将两个调用数据库多次的函数拆解成了异步。由于无需等待LOG_CLI数据库操作返回结果,响应速度得到提升。配合步骤1,对一个数据库的两次操作分解为对不同数据库的操作。

基于代码运行

基于集群组织

代码复用和微服务 中我们提供了服务内聚的解决方案。

通过kubernates和docker这样的集群服务我们可以将server层并行部署。

层次 节点
web应用 浏览器
server api服务 和 静态页面服务
db 数据库

由于各微服务对UserSite外键的引用,数据库是单库形式存在的。在server中,各服务依赖于base,所以也是聚合存在的。尽管可以并行部署多server节点,本质上还是单库单服务。

所以将各服务与user和site解耦,是分离多数据库和多服务的基础。各个服务独有的perm权限也可以拆分到这个服务的内部。这样在部署的时候,可以在服务层次上实现垂直聚合。

层次 节点\应用服务 web base github
server 服务器 vue静态页面 base github
db 数据库 base github

server就可以拆分为多套代码单独部署,每一个server配置专有数据库。对于base server和base库,由于它承载各服务的查询,这样负载最重的服务可以选用高配置,其他负载低的服务可以弹性增长或者按量配置。由于服务性能特点不同,对数据库和服务器的性能要求也不同。这种方案的话可以实现灵活和动态均衡。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant