Skip to content

Latest commit

 

History

History
342 lines (245 loc) · 14.6 KB

2019-04-03.md

File metadata and controls

342 lines (245 loc) · 14.6 KB

一. Algorithm

做了算法训练营布置的一道题目:50. Pow(x, n),自己实现一个求数的 N 次幂的函数。

按照皓叔说的, 先暴力搞出来再说,最先想到的就是循环相乘了,代码如下

public class Solution {
    double myPow(double x, int n) {
        int result = 1;
        for (int i = 1; i <= n; i ++) {
            result *= x;
        }
        return result;
    }
}

这种解法时间复杂度为 O(N),实际提交可能会超时,因此需要改进。最终的解决要用到数学知识:

a^m * a^n = a^(m+n)

因此以 2 的 10 次方为例:

2^10 = 2^5 * 2^5
2^5 = 2^2 * 2^2 * 2
2^2 = 2^1 * 2^1
2^1 = 2^0 * 2

因此每一个指数幂的计算都可以进行折半计算,这样就可以通过递归的形式进行计算。

递归公式为:

n 为偶数: x^n = x^(n/2) * x^(n/2)
n 为奇数: x^n = x^(n/2) * x^(n/2) * x

终止条件为:

n = 0, x^0 = 1

实现代码如下:

public class Solution {
    double myPow(double x, int n) { 
        
        double result = this.pow(x, Math.abs(n));
        
        if (n < 0) {
            return -n;
        }
        return n;
    }

    private double pow(double x, int n) {
        if (n == 0) {
            return 1;
        }
        double half = pow(x, n / 2);
        if (n % 2 == 0) {
            return half * half;
        } else {
            return half * half * x;
        }
    }
}

最终返回结果要考虑 n 为负数的情况,因此先取绝对值进行计算,如果为负数则返回计算结果的倒数。整个算法的复杂度由 O(N) 变为了 O(logN)。

二. Review

本周尝试读论文进度缓慢,分享一篇之前读的关于 Django 性能优化的文章吧:optimizing-django-orm-queries-for-best-performance

Django 框架提供了众多的 ORM 方法,当一个 Model 创建后,你可以通过其 API 进行数据的创建、更新、检索与删除。当 Model 发生变化的时候可以通过迁移完成对数据库的操作。总的来说,Django 中的查询操作是非常简单和直观的,但是如果没有采用合适的写法的话也会造成性能问题。

通过采用下面的技巧,我将数据库的查询次数降低了 92%,查询的执行时间降低了 80%。我将会在后面的文章中详细解释与讨论这些技巧。

一. 理解 QuerySet 是如何工作的

一个非常重要的事实是:Django QuerySet是懒执行的,只有访问到对应数据的时候,才会去访问数据库。另外如果你再次读取查询到的数据,将不会触发数据库的访问。

代码示例

# 这里不会访问数据
blogs = Blog.objects.filter(category='django')     

# 这里需要访问数据,因此会执行数据库查询
if blogs:
    print 'yes'

# 再次读取,并不会访问数据库
randomBlog = blogs[0]

了解了 QuerySet 的查询原理后,下面是一些可以提高查询性能的技巧。

二. 使用 select_related 或者 prefetch_related

大多数 Model 都定义了外键关联。当进行关联查询的时候,如果编写不正确,Django 将会多次访问库进行查询,我们应该避免这种情况。尤其是当查询语句位于某个循环中的时候,会导致只需要执行一次的查询重复执行多次。

来举一个例子进行说明,我们需要查询一个含有外键的表,并且要访问外键的数据。其 Model 定义如下:

Class Blog(models.Model):
    title = models.CharField(max_length=32)
    description = models.CharField(max_length=255)
    author = models.ForeignKey(Author)

Class Author(models.Model):
    name = models.CharField(max_length=32)
    email = models.EmailField(unique=True)
    city = models.CharField(max_length=32)

# 访问一次数据库
blog = Blog.objects.get(id=124)

# 再次访问数据库
author = blog.author

上面的代码访问了两次数据库,为了优化,可以通过 select_related 方法来查询外键关系,其实就是通过 join 来进行联结查询。我们可以将任何外键或者外键的列表传递给该方法,下面是使用示例:

# 访问数据库
blog = Blog.objects.select_related('author').get(id=124)

# 这里不会再次访问数据库,在之前的查询中已经取到了对应的数据
author = blog.author

除了 select_related() 方法之外还有与之类似的 prefetch_realated() 方法,这两个方法目的是一样的,但是实现策略有所不同。select_related 执行了一个 sql join 查询。prefetch_related 执行一个单独的查找,它允许预先读取多对多和多对一的对象数据,这是 select_related 做不到的。另外 perfetch_related 也可以与通用外键和关系一起使用。

三. 何时使用 count、len、exists

有几种使用不同数据的情况,比如检查某个特定的数据是否存在,获取数据的数量或者获取完整的数据。有几种方法可以达到目的,但是我们需要在不同情况下选择最优的解决方式。 让我们看下下面的例子:

1. 判断数据是否存在
# Bad query
blogs = Blog.objects.filter(category='django')  
exists = len(blogs)>0

# Bad Query
exists = Blog.objects.filter(category='django').count() > 0

# Good query 最佳实践
exists = Blog.objects.filter(category='django').exists()
2. 获取数据的数量
# Bad query
blogs = Blog.objects.filter(category='django')  
count = len(blogs)

# Good Query 最佳实践
count = Blog.objects.filter(category='django').count()

当然,如果你也需要用到具体的数据的话,更好的方式就是先获取到数据,然后在后面需要计算数量的时候在使用 len 进行计算。

四. 直接使用外键值

如果你只想获取外键的 id, 通过 fk_id 的方法获取要优于 fk.id 的方式。fk.id 的方式会为子表内容保存额外的数据库查询。下面是代码示例:

# Bad query, additional db lookup in author table
blog = Blog.objects.get(id=2)
author_id = blog.author.id  

# A better version of above query but still a bad query
blog = Blog.objects.select_related('author').get(id=2)
author_id = blog.author.id  

# Good Query 最佳实践
blog = Blog.objects.get(id=2)
author_id = blog.author_id

五. 不要查询不需要的值

如果你明确知道需要使用的数据,那就没没必要查询数据库中所有列的数据。下面是几种实现方式。

1. 使用 values 和 values_list

如果一个表中的列非常多,而你只需要其中的一部分内容,可以通过 queryset.values() 与 queryset.values_list() 来获取指定列的字典或者列表,。values() 返回的是以字典的形式返回每一条数据,而 values_list() 返回的是每条数据元组的列表。

示例代码如下,获取分类为 django 的 Blog 的 title 和 author 的 name。通过 values() 和 values_list() 可以提高查询速度。

# Bad query
blogs = Blog.objects.filter(category='django').select_related('author')
my_list = []
for blog in blogs:
   mylist.append({'title':blog.title, 'author_name':blog.author.name}) 

# Good Query 最佳实践
mylist = Blog.objects.filter(category='django').values('title', 'author__name')
2. 使用 queryset.only 与 difer

也可以通过 queryset.only 与 difer 方法来优化查询性能。通过 only 方法可以只查询指定的列。而 difer 查询除了指定列之外的其他列。 代码示例如下:

# If you want only author and blog title 

# Bad query, this will retrieve all details
blogs = Blog.objects.filter(category='django').select_related('author')

# Good Query 最佳实践,只查询 Blog 的 title 与 author
blogs = Blog.objects.filter(category='django').only('author','title').select_related('author')

# If you want everything apart from description

# Bad query, this will retrieve all details
blogs = Blog.objects.filter(category='django').select_related('author')

# Good Query 最佳实践,查询除了 description 外 Blog 的其他字段值
blogs = Blog.objects.filter(category='django').defer('description').select_related('author')
3. 分析创建表索引

除了上面的优化技巧之外,一个更重要的事情就是要彻底理解你的数据。你应该深刻的了解你的数据结构和表的查询情况。了解之后就可以通过建立合适的索引来提高性能。记住,索引会降低写的性能,因此也要将这种情况考虑在内,最终得出合适的方案。

六. 安装 django-debug-tool 分析 Ajax 请求

django-debug-tool 是一个功能非常强大的 debug 插件,它可以展现项目中每次查询所执行的 SQL 语句的执行时间。这对我们在众多查询中找到比较耗时的查询非常有帮助。通过安装 django-debug-panal 插件或者其 chrome 插件也可以对 Ajax 请求进行分析。下面是其安装步骤:

pip install django-debug-toolbar
pip install django-debug-panel

# Add these to installed apps 
INSTALLED_APPS = (
    .....
    'debug_toolbar',
    'debug_panel',
    .....
)

# Add a middleware class
MIDDLEWARE_CLASSES += ('debug_panel.middleware.DebugPanelMiddleware',)

# Install this chrome extension
chrome extension(https://chrome.google.com/webstore/detail/django-debug-panel/nbiajhhibgfgkjegbnflpdccejocmbbn?hl=en)

在我所执行的查询中,共执行了 305 次查询,耗费了 23.2 秒

优化之前: 优化之前的性能

优化之后: 优化之后的性能

优化前后的性能差异非常巨大,执行时间从 26.2 降到了 3.8 秒,降低了 85.4%。造成性能的主要原因是在循环中进行了外键访问,导致了大量查询的执行。该实例是在一个很小的开发数据库中测试的。在生产中,执行时间从 3.6 秒降到了 3.65 秒,耗时降低了 82%。

三. Tips

kubeadm init 时拉取镜像,因为网络原因导致其所需的镜像没法正常下载下来,为了解决这个问题,常用的方式有几种:

  • 让你的机器可以真正的上网
  • 修改镜像路径
  • 提前下载镜像

分享下第三种方式,一位大神搭建了一个项目 gcr.io_mirror,将 K8S 所需的镜像下载下来传到了 DockerHub 中方便我们去下载,镜像下载语法如下:

// k8s.gcr.io 替换为了  gcr.azk8s.cn/google-containers 
docker pull gcr.azk8s.cn/google-containers/federation-controller-manager-arm64:v1.3.1-beta.1 
# eq
docker pull gcr.io/google-containers/federation-controller-manager-arm64:v1.3.1-beta.1 

# special
# eq 
docker pull k8s.gcr.io/federation-controller-manager-arm64:v1.3.1-beta.1

K8S 所需的镜像大多是从 k8s.gcr.io 源下载的,可以通过下面命令查看:

$ kubeadm config images list
k8s.gcr.io/kube-apiserver:v1.14.1
k8s.gcr.io/kube-controller-manager:v1.14.1
k8s.gcr.io/kube-scheduler:v1.14.1
k8s.gcr.io/kube-proxy:v1.14.1
k8s.gcr.io/pause:3.1
k8s.gcr.io/etcd:3.3.10
k8s.gcr.io/coredns:1.3.1

基于上面提到的下载语法,可以编写脚本将镜像从 DockerHub 上下载下来然后重新打标签,这样 kubeadm init 的时候就无需再次下载镜像了,脚本代码如下:

#!/bin/bash
# 基于上面命令列出镜像
images=(
		kube-apiserver:v1.14.1
		kube-controller-manager:v1.14.1
		kube-scheduler:v1.14.1
		kube-proxy:v1.14.1
		pause:3.1
		etcd:3.3.10
		coredns:1.3.1
       )

for imageName in ${images[@]} ; do
# 从 DockerHub 中下载
docker pull gcr.azk8s.cn/google-containers/$imageName
# 重新打 tag
docker tag gcr.azk8s.cn/google-containers/$imageName k8s.gcr.io/$imageName
# 删除旧的镜像
docker rmi gcr.azk8s.cn/google-containers/$imageName
done

四. Share

分享下池老师的文章吧:人生中的谎言你听过几个?。文章说的非常中肯,很多看似正确的事情其实是不折不扣的人生谎言,比如努力一定会成功,生活会变得越来越好。分享下自己的几点想法吧。

1. 关于努力

努力本身是个非常模糊的词语,少了太多的限定条件。如果方向错了,再多的努力也没用,反而会加速失败。而如果是使蛮力,那只会事倍功半,把自己折磨的疲惫不堪。 生活中所需要的不是毫无前置条件的努力,而是不断的思考自己的发展方向,坚持用正确的方式做对的事情。如果某些时候迷茫了,不知道该做什么,那就想一下哪些是可能会让你在未来变好的事情,比如健身,比如学习,阅读、写作,比如锻炼自己的沟通与表达能力。这些不一定会百分百保证你成功,但可能会让你大概率的在机遇到来时有能力把握住。

2. 运气起的作用可能更大

接上一条,很多人相信努力就会成功,但现实并不是如此,有时候哪怕你方向对了,也足够的聪明与勤奋,但是可能因为机遇不对或者其他的原因,成功依然离你很远。进入一个赛道,时间早了成为炮灰,晚了可能汤都喝不上。而这些所谓的机遇,时机,其实就是运气,专业点讲就是随机性。你也不知道哪天幸运女神会眷顾到你,马爸爸如果不去西雅图讨一次债见到互联网可能就不会有现在的阿里巴巴了吧,哪怕马爸爸依然会做成一番事业,但可能远远达不到现在阿里巴巴的高度。

3. 事情永远都在

文章中提到的一大谎言是忙完这一阵就好了,但现实生活中往往是这一阵还没忙完,下一阵已经提前准备好了,时间看起来始终不够。但就个人观察而言,现实生活中的时间不够用往往不是因为事情太多,而是因为两点:时间利用率太低,无谓的时间浪费过多。时间利用率太低包括做事的方法不对,比如习惯采用并发模式做事,看起来做了很多事情,实则降低了效率。时间浪费就更不用说了,各种社交网络,各种瓜不断瓜分这我们的时间。你如果拿时间去刷微博、朋友圈,去论坛逛水,自然就没时间去看书。如果你沉迷刷剧,自然业余时间就没有时间去学习提高。事情永远都在,重要的不是忙完这一阵就好了,而是始终提醒自己去做最重要的事情,最重要的事情以及内心真正想做的事。