啥是docker?

了解docker之前,先让我们了解几个概念:

  • 虚拟化技术是一种将计算机物理资源进行抽象、转换为虚拟的计算机资源提供给程序使用的技术。
  • 知道一点计算机原理:程序对计算机资源的调用主要依赖于操作系统所给出的接口。我们的程序通过操作系统提供的接口,向物理硬件发送指令。
  • 为程序跨平台兼容而生,要实现程序跨平台兼容的方法其实很简单,只要操作系统或者物理硬件所提供的接口调用方式一致,程序便不需要兼容不同硬件平台的接口,而只需要针对这一套统一的接口开发即可。
  • 将虚拟化应用于资源管理,虚拟化技术能够提高计算机资源的使用率,是指利用虚拟化,我们可以将原来程序用不到的一些资源拿出来,分享给另外一些程序,让计算机资源不被浪费。这里要注意了,我们所说的是提高计算机资源使用率,而非减少程序资源的占用率,这两者看似很相近,其实并非是同一个概念。
  • 虚拟机,通常来说就是通过一个虚拟机监视器 ( Virtual Machine Monitor ) 的设施来隔离操作系统与硬件或者应用程序和操作系统,以此达到虚拟化的目的。这个夹在其中的虚拟机监视器,常常被成为 Hypervisor。
  • 容器技术,指的是操作系统自身支持一些接口,能够让应用程序间可以互不干扰的独立运行,并且能够对其在运行中所使用的资源进行干预。由于应用程序的运行被隔离在了一个独立的运行环境之中,这个独立的运行环境就好似一个容器,包裹住了应用程序,这就是容器技术名字的由来。

再谈docker:Docker 项目是一个由 Go 语言实现的容器引擎,它最初由 dotCloud 这家做云服务的公司在 2013 年开源。使用 Docker 后,开发者能够在本地容器中得到一套标准的应用或服务的运行环境,由此可以简化开发的生命周期 ( 减少在不同环境间进行适配、调整所造成的额外消耗 )。

Docker 的实现,主要归结于三大技术:命名空间 ( Namespaces ) 、控制组 ( Control Groups ) 和联合文件系统 ( Union File System ) 。

  • Linux 内核的命名空间,就是能够将计算机资源进行切割划分,形成各自独立的空间。Linux Namespaces 可以分为很多具体的子系统,如 User Namespace、Net Namespace、PID Namespace、Mount Namespace 等等。
  • 资源控制组的作用就是控制计算机资源的。虚拟化除了制造出虚拟的环境隔离同一物理平台运行的不同程序之外,另一大作用就是控制硬件资源的分配,CGroups 的使用正是为了这样的目的。
  • 联合文件系统 ( Union File System ) 是一种能够同时挂载不同实际文件或文件夹到同一目录,形成一种联合文件结构的文件系统。AUFS(Advanced Union File System ) 将文件的更新挂载到老的文件之上,而不去修改那些不更新的内容,这就意味着即使虚拟的文件系统被反复修改,也能保证对真实文件系统的空间占用保持一个较低水平。

Docker的理念:Docker 推崇一种轻量级容器的结构,即一个应用一个容器。

Docker核心组成

  • 镜像:所谓镜像,可以理解为一个只读的文件包,其中包含了虚拟环境运行最原始文件系统的内容。
  • 容器:如果把镜像理解为编程中的类,那么容器就可以理解为类的实例。镜像内存放的是不可变化的东西,当以它们为基础的容器启动后,容器内也就成为了一个“活”的空间。容器由三项组成:一个 Docker 镜像;一个程序运行环境;一个指令集合。
  • 网络:网络通讯是目前最常用的一种程序间的数据交换方式。在 Docker 中,实现了强大的网络功能,我们不但能够十分轻松的对每个容器的网络进行配置,还能在容器间建立虚拟网络,将数个容器包裹其中,同时与其他网络环境隔离。
  • 数据卷:文件也是重要的进行数据交互的资源。在 UnionFS 的加持下,除了能够从宿主操作系统中挂载目录外,还能够建立独立的目录持久存放数据,或者在容器间共享。在 Docker 中,通过这几种方式进行数据共享或持久化的文件或目录,我们都称为数据卷 ( Volume )。
  • Docker Engine:工业级的容器引擎 ( Industry-standard Container Engine )。在 Docker Engine 中,实现了 Docker 技术中最核心的部分,也就是容器引擎这一部分。
  • docker daemon 和 docker CLI:所有我们通常认为的 Docker 所能提供的容器管理、应用编排、镜像分发等功能,都集中在了 docker daemon 中,而我们之前所提到的镜像模块、容器模块、数据卷模块和网络模块也都实现在其中。在 docker daemon 管理容器等相关资源的同时,它也向外暴露了一套 RESTful API,我们能够通过这套接口对 docker daemon 进行操作。在 Docker Engine 里还直接附带了 docker CLI 这个控制台程序用以控制docker daemon。docker daemon 和 docker CLI 所组成的,正是一个标准 C/S ( Client-Server ) 结构的应用程序。

好了,废话说完了,开始我们快乐的使用docker吧!(这里只讲一下mac的操作方法。)

mac桌面下载地址

将存有n个人的电话簿按照人名排序,时间复杂度是多少?

每次需要检查元素中n个元素,需要的时间为O(n)。这样的操作需要操作n次,所以时间复杂度为O(n x n),即O (n 2 )。
但实际上第一次需要检查n 个元素,但随后检查的元素数依次为n - 1, n – 2, …, 2和1。平均每次检查的元素数为1/2 × n ,因此运行时间为O (n × 1/2 × n )。但大O表示法省略诸如1/2这样的常数,因此简单地写作O(n x n)。

选择排序原理

在要排序的一组数中,选出最小的一个数与第一个位置的数交换。然后在剩下的数当中再找最小的与第二个位置的数交换,如此循环到倒数第二个数和最后一个数比较为止。

选择排序代码实现

 /**
  * 选择排序.
  *
  * @param  array $value 待排序数组
  *
  * @return array
  */
  function select_sort(&$value=[])
  {
    $length = count($value)-1;
    for ($i=0; $i < $length; $i++) {
      $point = $i;// 最小值索引
      for ($j=$i+1; $j <= $length; $j++) {
        if ($value[$point] > $value[$j]) {
          $point = $j;
        }
      }
      $tmp = $value[$i];
      $value[$i] = $value[$point];
      $value[$point] = $tmp;
    }
    return $value;
  }

冒泡排序原理

在要排序的一组数中,对当前还未排好的序列,从前往后对相邻的两个数依次进行比较和调整,让较大的数往下沉,较小的往上冒。即,每当两相邻的数比较后发现它们的排序与排序要求相反时,就将它们互换。

冒泡排序代码实现


  /**
   * 冒泡排序
   * bubble sort algorithm
   * 
   * @param  array $value 待排序数组 the array that is waiting for sorting
   * @return array
   */
  function bubble($value = [])
  {
      $length = count($value) - 1;
      // 外循环
      // outside loop
      for ($j = 0; $j < $length; ++$j) {
          // 内循环
          // inside loop
          for ($i = 0; $i < $length; ++$i) {
              // 如果后一个值小于前一个值,则互换位置
              // if the next value is less than the current value, exchange each other.
              if ($value[$i + 1] < $value[$i]) {
                  $tmp = $value[$i + 1];
                  $value[$i + 1] = $value[$i];
                  $value[$i] = $tmp;
              }
          }
      }
      return $value;
  }
  /**
   * 优化冒泡排序
   * optimized bubble sort algorithm
   * 
   * @param  array $value 待排序数组 the array that is waiting for sorting
   * @return array
   */
  function bubble_better($value = [])
  {
    $flag   = true; // 标示 排序未完成 the flag about the sorting is whether or not finished.
    $length = count($value)-1; // 数组最后一个元素的索引 the index of the last item about the array.
    $index  = $length; // 最后一次交换的索引位置 初始值为最后一位 the last exchange of index position, default value is equal to the last index.
    while ($flag) {
      $flag = false; // 假设排序已完成 let's suppose the sorting is finished.
      for ($i=0; $i < $index; $i++) {
        if ($value[$i] > $value[$i+1]) {
          $flag  = true; // 如果还有交换发生,则排序未完成  if the exchange still happen, it show that the sorting is not finished. 
          $last  = $i; // 记录最后一次发生交换的索引位置 taking notes the index position of the last exchange.
          $tmp   = $value[$i];
          $value[$i] = $value[$i+1];
          $value[$i+1] = $tmp;
        }
      }
      $index = !$flag ? : $last;
    }
    return $value;
  }

内存的工作原理

有一个存储柜,存储柜有很多抽屉,每个抽屉均有自己的编号:fe0ffeeb(一个内存单元的地址),每个抽屉可以放一样东西。现在你要存两样东西,因此你向柜台要了两个抽屉(需要将数据存储到内存时,你请求计算机提供存储空间,计算机给你一个存储地址)。

用数组还是链表?

有时候,需要在内存中存储一系列元素。假设你要编写一个管理待办事项的应用程序,为此需要将这些待办事项存储在内存中。应使用数组还是链表呢?

数组

数组的特点:

  • 数组中的每个元素在内从中都是相连的
  • 数组的元素带编号,编号从0开始

鉴于数组更容易掌握,我们先将待办事项存储在数组中。使用数组意味着所有待办事项在内存中都是相连的(紧靠在一起的)。现在我们有四个代办事项,要放在抽屉,但是后面的抽屉被别人占用了。

1547023972659.jpg

就像是占连在一排的座位,原来的四连坐有一个位置被占了,我们只能另找座位(数组在存储或者新增元素时如果没了空间,就得移到内存其他地方)。
基于数组连在一起的缺点,我们可以”预留座位“,但是这样做也有缺点:

  • 你额外请求的位置可能根本用不上,这将浪费内存。你没有使用,别人也用不了。
  • 待办事项超过10个后,你还得转移。

鉴于这种情况,我们使用链表!

链表

链表有两个特点:

  • 链表中的元素可存储在内存的任何地方。
  • 链表的每个元素都存储了下一个元素的地址,从而使一系列随机的内存地址串在一起。

这犹如寻宝游戏。你前往第一个地址,那里有一张纸条写着“下一个元素的地址为123”。因此,你前往地址123,那里又有一张纸条,写着“下一个元素的地址为847”,以此类推。在链表中添加元素很容易:只需将其放入内存,并将其地址存储到前一个元素中。

两者优劣点

  • 链表存在类似的问题。在需要读取链表的最后一个元素时,你不能直接读取,因为你不知道它所处的地址,必须先访问元素#1,从中获取元素#2的地址,再访问元素#2并从中获取元素#3的地址,以此类推,直到访问最后一个元素。需要同时读取所有元素时,链表的效率很高:你读取第一个元素,根据其中的地址再读取第二个元素,以此类推。但如果你需要跳跃,链表的效率真的很低。
  • 数组与此不同:你知道其中每个元素的地址。例如,假设有一个数组,它包含五个元素,起始地址为00,那么元素#5的地址是多少呢?

数组和链表操作的运行时间

-数组链表
读取O(1)O(n)
插入O(n)O(1)
删除O(n)O(1)

  • 从一个 Bug 讲起
  • 我看到的乱象
  • 我推荐的解决方案

一个奇怪的 Bug

  • 网盘的上传服务
    Windows 下 IE, Chrome 浏览器下正常

Linux 下 Firefox, Chrome 浏览器下正常
Windows 下的 Firefox 不正常, 上传失败。

如何定位Bug

  • 首先查看浏览器的控制台,观察是否有错误(都没有)
  • 用 Wireshark 截获请求包,发现失败的案例服务端返 回412(通常为200)
  • 对比 HTTP Request,发现失败案例的 HTTP 头有 “Content-length”,而其他情况下为 “Content- Length”
  • 用 telnet 发送不同的请求来验证结论
  • 确认原因是服务端对 Content-Length 的 大小写敏感,与标准不符,造成了这个 Bug
  • 通知服务端开发人员尽快修复

好Bug/坏Bug

  • 能再现的 Bug 就是好 Bug
  • 如果不能再现,能拿到异常栈或者详细 日志的也是好Bug
  • 对于坏 Bug, 他的最大用处就是督促你补 充日志

另一个奇怪的Bug

  • 从公司访问网站有12s延迟
  • 直接 ping 响应很快
  • 从其他网络来源访问没有问题
  • 监控没有问题, 也没有客户来抱怨此事

先猜猜看

  • 公司路由出问题了?
    重启一下路由试试看?

但为什么只有访问我们自己网站会出事?

  • 人品问题?
    做程序员还是唯物一点比较好
  • 不管,反正没人抱怨这事
    99% 的用户会遇到问题时不会报告

分析

  • tcpdump抓包
    客户端: 发了6个 SYN 包,到最后一个才收到 SYN/ACK

服务端: 确实收到了6个 SYN 包, 而且确实前面5 个 SYN 包都没有回复
从其他网络访问,确实在首个 SYN 包之后返回

  • 结论: 问题跟路由/线路无关,确实是服务端的问题
  • 再重新分析tcpdump的记录,发现前SYN 包带了 timestamp, 第6个没带
    尝试关掉客户端 TCP timestamp, 瞬间响应

尝试关掉服务端 TCP timestamp, 瞬间响应

  • 结论: Bug 与 timestamp 相关,但仍然无法 解释为什么其他网络没问题

解决方案A

  • 关掉服务端的 TCP timestamp
    TCP timestamp 会影响传输速度,但影 响不大
  • 疑问仍然存在,为什么服务器不响应某 些 timestamp 包, 有时候又能正常响应
  • 看内核源码 (CTO 亲自操刀)
    入口机器有大量的反向代理,端口经常不够用,所以开 启了TIME_WAIT 端口重用

开启了 TIME_WAIT 端口重用后,服务端要求同一个IP的 SYN 包 timestamp 必须是顺序的
办公网络是内网, 并且对我们官网访问很频繁,导致故障发生

  • 看来找到真实的原因了, 有没有更好地解决方案?

解决方案B

  • nginx 高版本已经支持 keep-alive
  • 升级 nginx, 启用 keep-alive, 降低端口占用
  • 关闭端口重用,并加强端口数的监控和 报警

我所看到的乱象

  • 用寻找 workaround 代替解决问题
    换个浏览器吧

重启, 清 cookie, 清 cache, 重新登录
现场被破坏得干干净净
对于那些不易再现的 Bug, 就损失了一次修 复的机会

  • 尝试用试错法解决所有问题
    升级依赖包, 升级插件, ...

随便猜一个原因,改两句代码,然后 重新测试

  • 缺少反省
    寻找到 workaround 便认为问题解决了

没有用测试固化 Bug, 容易产生回归Bug
一个 bug 可能在多处出现,没有尝试搜索其他有 Bug 的地方
没有通过补充日志等手段来降低日后定位Bug的难度

  • 海恩法则: 每一起严重事故的背后,必然 有29次轻微事故和300起未遂先兆以及 1000起事故隐患

我们推荐的方法

  • 出了问题不要靠猜
    浏览器

服务端日志
抓包工具

浏览器看什么?

  • 是否有 JS 错误?
  • 网络请求是否发出?
  • 发出的请求是否正确: URL, 方法, 参数, Accept, Cookie
  • 期望的返回值是什么?
  • Request-Id

Request-Id

  • 对每次请求产生一个唯一的Id
  • 该 Id 以 HTTP Response Header 的方式 发送到客户端
  • 可以在 nginx 层面实现或者在业务逻辑层 面实现

服务端日志看什么?

  • 四要素:
    时间: 开始时间,总耗时

谁: 用户Id, Session-Id, Request-Id
做什么:URL, 方法,XHR?, format, 参数(注意保护密码)
结果是什么?

  • 对于其他服务的请求
    邮件,短信,其他服务...

记录请求详情和耗时

但是经常没有足够的日志

wireshark tcpdump

  • 抓包工具
  • wireshark: 有界面
  • tcpdump: sudo tcpdump -n -s 4096 -w 1.log port 80

其他工具

  • strace/dtruss: 系统调用跟踪工具
  • lsof: 列出文件打开情况
  • valgrind: 查内存泄露
  • ltrace: 查询库调用

总结

  • 我们推崇通过一种系统的方法来分析问题,寻找问题的根源
  • 我们反对只靠试错法来解决问题
  • 能再现的 Bug 是好 Bug, 如果不能再现,也要拿到 对应的网络请求和日志
  • 如果这次解决不了 Bug, 那么就改善你的日志,确 保下次 Bug 出现的时候能解决他

摘自https://github.com/lidaobing

特点:

1.有序元素
2.每次从中间值开始查

工作原理

1234……99100

假使有一列1~100的元素,我随机想一个元素,让你猜。你的目标是以最小的次数猜到这个数字。你每次猜测后,我都会说大了、小了或对了。

1.假使你从1开始猜,每次猜测只能排除一个数字,如果我想的是100,你得猜一百次。
2.二分查找,从50开始猜测。你猜50,我说小了,说明数字在50~100之间。你猜75,我说小了,又排除一半数据。这样你便可以在7次就能找到准确的数据!因为每次都会排除很多数据。(这也解释了,为什么二分查找算法只适用于有序元素)

问题:1~1024这列元素,需要猜几次呢?

答案是10次,$\log_2{1024}$,为什么呢?因为在使用二分查找时,每次排除一半元素,直到最后只剩下一个元素,所以二分查找最多需要$\log_2{n}$步。
这里普及一下数学知识,对数运算是幂运算的逆运算,也就是说$\2^10$=1024 <=> $\log_2{1024}$=10。

代码展现形式

<?php
//二分查找法
function binSearch($arr,$search){
    $height=count($arr)-1;
    $low=0;
    while($low<=$height){
        //由于php取商是有小数的,所以向下取整,不过也可不加,数组也会取整
        $mid=floor(($low+$height)/2);//获取中间数
        if($arr[$mid]==$search){
            return $mid;//返回
        }elseif($arr[$mid]<$search){//当中间值小于所查值时,则$mid左边的值都小于$search,此时要将$mid赋值给$low
            $low=$mid+1;
        }elseif($arr[$mid]>$search){//中间值大于所查值,则$mid右边的所有值都大于$search,此时要将$mid赋值给$height
            $height=$mid-1;
        }
    }
    return "查找失败";
} 
//二分查找递归实现
function binSearch2($arr,$low,$height,$k){
    if($low<=$height){
        $mid=floor(($low+$height)/2);//获取中间数
        if($arr[$mid]==$k){
            return $mid;
        }elseif($arr[$mid]<$k){
            return binSearch2($arr,$mid+1,$height,$k);
        }elseif($arr[$mid]>$k){
            return binSearch2($arr,$low,$mid-1,$k);
        }
    }
    return -1;
}
 
//顺序查找
function seqSearch($arr,$k){
    foreach($arr as $key=>$val){
        if($val==$k){
            return $key;
        }
    }
    return -1;
}

那么二分查找法的时间复杂度是多少呢?

假设检查一个元素需要1毫秒。使用简单查找时,必须检查100个元素,因此需要100毫秒才能查找完毕。使用大O表示法,这个运行时间怎么表示呢?,时间为O(n)。
二分查找则为O($\log_2{n}$),即O (log n )。