技术

如何使用RedisTemplate访问Redis数据结构 MySQL重要知识点 OAuth2认证授授权流程 分布式锁 服务调用 MQ的介绍 SpringCloud 使用链 Eureka 的点对点通信 介绍Eureka RabbitMQ与其它MQ的对比 Springboot 启动过程分析 Springboot 入门 Linux内存管理 自定义CNI IPAM 扩展Kubernetes 副本一致性 spring redis 源码分析 kafka实践 spring kafka 源码分析 Linux进程调度 让kafka支持优先级队列 Codis源码分析 Redis源码分析 C语言学习 《趣谈Linux操作系统》笔记 Kubernetes安全机制 jvm crash分析 Prometheus 学习 Kubernetes监控 Kubernetes 控制器模型 容器日志采集 容器狂占cpu怎么办? 容器狂打日志怎么办? Kubernetes资源调度-scheduler 时序性数据库介绍及对比 influxdb入门 maven的基本概念 《Apache Kafka源码分析》——server Kubernetes objects之编排对象 源码分析体会 自动化mock AIOps说的啥 从DevOps中挖掘docker的价值 《数据结构与算法之美》——算法新解 Kubernetes源码分析——controller mananger Kubernetes源码分析——apiserver Kubernetes源码分析——kubelet Kubernetes整体结构 ansible学习 Kubernetes源码分析——从kubectl开始 jib源码分析之Step实现 kubernetes实践 线程排队 jib源码分析之细节 从一个签名框架看待机制和策略 跨主机容器通信 jib源码分析及应用 docker环境下的持续构建 docker环境下的持续发布 一个容器多个进程 kubernetes yaml配置 marathon-client 源码分析 《持续交付36讲》笔记 程序猿应该知道的 mybatis学习 无锁数据结构和算法 《Container-Networking-Docker-Kubernetes》笔记 活用linux 命令 为什么很多业务程序猿觉得数据结构和算法没用? 串一串一致性协议 当我在说PaaS时,我在说什么 《数据结构与算法之美》——数据结构笔记 swagger PouchContainer技术分享体会 harbor学习 用groovy 来动态化你的代码 《深入剖析kubernetes》笔记 精简代码的利器——lombok 学习 java 语言的动态性 rxjava3——背压 rxjava2——线程切换 spring cloud 初识 JVM4——《深入拆解java 虚拟机》笔记 《how tomcat works》笔记 commons-pipeline 源码分析 hystrix 学习 rxjava1——概念 Redis 学习 TIDB 学习 分布式计算系统的那些套路 Storm 学习 AQS3——论文学习 Unsafe Spark Stream 学习 linux 文件系统 mysql 批量操作优化 《自己动手写docker》笔记 java8 实践 中本聪比特币白皮书 细读 区块链泛谈 比特币 大杂烩 总纲——如何学习分布式系统 forkjoin 泛谈 hbase 泛谈 看不见摸不着的cdn是啥 《jdk8 in action》笔记 程序猿视角看网络 calico 问题排查 bgp初识 mesos 的一些tips mesos 集成 calico calico AQS2——粗略的代码分析 我们能用反射做什么 web 跨域问题 《clean code》笔记 compensable-transaction 源码分析 硬件对软件设计的影响 elasticsearch 初步认识 mockito简介及源码分析 线上用docker要解决的问题 《Apache Kafka源码分析》——Producer与Consumer 停止容器 dns隐藏的一个坑 《mysql技术内幕》笔记2 《mysql技术内幕》笔记1 log4j学习 为什么netty比较难懂? 回溯法 apollo client源码分析及看待面向对象设计 java系并发模型的发展 从一个marathon的问题开始的 docker 环境(主要运行java项目)常见问题 Scala的一些梗 OpenTSDB 入门 spring事务小结 事务一致性 javascript应用在哪里 netty中的future和promise 《netty in action》读书笔记 netty对http2协议的解析 ssl证书是什么东西 一些tricky的code http那些事 苹果APNs推送框架pushy apple 推送那些事儿 编写java框架的几大利器 JVM3——java内存模型 java concurrent 工具类 java exception java io涉及到的一些linux知识 network channel network byte buffer 测试环境docker化实践 通用transport层框架pigeon netty(七)netty在框架中的使用套路 Nginx简单使用 《Linux内核设计的艺术》小结 从Go并发编程模型想到的 mesos深入 Macvlan Linux网络源代码学习2 《docker源码分析》小结 对web系统的一些理解 docker中涉及到的一些linux知识 hystrix学习 Linux网络源代码学习 Docker网络五,docker网络的回顾 zookeeper三重奏 数据库的一些知识 Spark 泛谈 commons-chain netty(六)netty回顾 Thrift基本原理与实践(三) Thrift基本原理与实践(二) Thrift基本原理与实践(一) Future 回调 Docker0.1.0源码分析 基于spring boot和Docker搭建微服务 通过Docker Plugin来扩展Docker Engine java gc Docker网络四,基于Centos搭建Docker跨主机网络 google guava的一些理解 Jedis源码分析 Redis概述 Docker回顾 深度学习是个什么鬼 Docker网络三,基于OVS实现Docker跨主机网络 Linux网络命令操作 JTA与TCC 换个角度看待设计模式 Scala初识 netty(四)netty对http协议的实现(废弃) netty(三)netty框架泛谈 向Hadoop学习NIO的使用 以新的角度看数据结构 AQS1——并发相关的硬件与内核支持 使用Ubuntu要做的一些环境准备 Docker网络二,libnetwork systemd 简介 那些有用的sql语句 异构数据库表在线同步 spring aop 实现原理简述——背景知识 quartz 源码分析 基于docker搭建测试环境(二) spring aop 实现原理简述 我们编程的那些潜意识 自己动手写spring(八) 支持AOP 自己动手写spring(七) 类结构设计调整 分析log日志 一次代码调试的过程 自己动手写spring(六) 支持FactoryBean 自己动手写spring(九) 总结 自己动手写spring(五) bean的生命周期管理 自己动手写spring(四) 整合xml与注解方式 自己动手写spring(三) 支持注解方式 自己动手写spring(二) 创建一个bean工厂 自己动手写spring(一) 使用digester varnish 简单使用 docker volume 关于docker image的那点事儿 基于docker搭建测试环境 分布式配置系统 JVM2——JVM和传统OS对比 git spring rmi和thrift maven/ant/gradle使用 再看tcp mesos简介 缓存系统——具体组件 缓存系统 java nio的多线程扩展 多线程设计模式/《Concurrency Models》笔记 回头看Spring IOC IntelliJ IDEA使用 Java泛型 vagrant 使用 Go 常用的一些库 Netty(一)初步了解 java mina Golang开发环境搭建(Windows下) java nio入门 ibatis自动生成类和文件 Python初学 Goroutine 调度模型猜想 一些编程相关的名词 虚拟网络 《程序员的自我修养》小结 VPN(Virtual Private Network) Hadoop安装与调试 Kubernetes持久化存储 Kubernetes 其它特性 访问Kubernetes上的服务 Kubernetes副本管理 Kubernetes pod 组件 使用etcd + confd + nginx做动态负载均衡 nginx安装与简单使用 在CoreOS集群上搭建Kubernetes 如何通过fleet unit files 来构建灵活的服务 CoreOS 安装 定制自己的boot2docker.iso CoreOS 使用 Go初学 JVM1——jvm小结 硬币和扑克牌问题 LRU实现 virtualbox 使用 os->c->java 多线程 容器类概述 zabbix 使用 zabbix 安装 Linux中的一些点 关于集群监控 ThreadLocal小结 我对Hadoop的认识 haproxy安装 docker快速入门

标签


硬件对软件设计的影响

2018年01月07日

简介

物质基础决定上层建筑,在计算机世界里,硬件作为“物质基础”深刻的影响着软件设计和性能。

国外一个大牛的博客 Mechanical Sympathy Hardware and software working together in harmony 讲的是底层硬件是如何运作的,以及与其协作而非相悖的编程方式。剖析Disruptor:为什么会这么快?(二)神奇的缓存行填充 作为一个开发者你可以逃避不去了解CPU、数据结构或者大O符号 —— 而我用了10年的职业生涯来忘记这些东西。但是现在看来,如果你知道这些知识并应用它,你能写出一些非常巧妙和非常快速的代码。

2019.4.12 补充:进程管理信息数据结构 二进制文件分段 ==> 进程分段 ==> 指令操作码/操作数 ==> cpu运算单元/数据单元 ==> cpu代码段寄存器/数据段寄存器/堆栈段寄存器等,从这个视角看,又有一种软硬件融合的味道。

为什么需要反码和补码

有界/越界/溢出与取模

在数学的理论中,数字可以有无穷大,也有无穷小。现实中的计算机系统不可能表示无穷大或者无穷小的数字,都有一个上限和下限。加法加越界了就成了 取模运算。

符号位

在实际的硬件系统中,**计算机 CPU 的运算器只实现了加法器88,而没有实现减法器。那么计算机如何做减法呢?我们可以通过加上一个负数来达到这个目的。如何让计算机理解哪些是正数,哪些是负数呢?人们把二进制数分为有符号数(signed)和无符号数(unsigned)。如果是有符号数,那么最高位就是符号位。如果是无符号数,那么最高位就不是符号位,而是二进制数字的一部分。有些编程语言,比如 Java,它所有和数字相关的数据类型都是有符号位的;而有些编程语言,比如 C 语言,它有诸如 unsigned int 这种无符号位的数据类型。

比取模更“狠”——有符号数的溢出

对于 n 位的数字类型,符号位是 1,后面 n-1 位全是 0,我们把这种情形表示为 -2^(n-1)。n 位数字的最大的正值,其符号位为 0,剩下的 n-1 位都1,再增大一个就变为了符号位为 1,剩下的 n-1 位都为0。也就是n位 有符号最大值 加1 就变成了 n位有符号数界限范围内最小的负数——上溢出之后,又从下限开始

是不是有点扑克牌的意思, A 可以作为10JQKA 的最大牌,也可以作为A23456 的最小牌。

  下限 上限
n位无符号数 0 2^n-1
n位有符号数 -2^(n-1) 2^(n-1)-1

取模 可以将(最大值+1) 变成下限值,对于无符号数是0 ,对于有符号数是负数。

减法靠补码

原码就是我们看到的二进制的原始表示,是不是可以直接使用负数的原码来进行减法计算呢?答案是否定的,因为负数的原码并不适用于减法操作(加负数操作)

因为取模的特性,我们知道 i = i + 模数。 那么 i-j = i-j + 模数 也是成立的,进而i-j = i + (模数 -j)模数 -j 即补码 可以对应到计算机的 位取反 和 加 1 操作

本质就是

  1. 加法器不区分 符号位和数据位
  2. 越界 等于 取模,对于有符号位的取模,可以使得 正数 变成负数

我们经常使用朴素贝叶斯算法 过滤垃圾短信,P(A|B)=P(A) * P(B/A) / P(B) 这个公式在数学上平淡无奇,但工程价值在于:实践中右侧数据比 左侧数据更容易获得。 cpu减法器也是类似的道理,减法器 = CPU 位取反 + 加法器

CPU和内存

Java和操作系统交互细节

RAM 是随机访问内存,就是给一个地址就能访问到数据,而磁盘这种存储媒介必须顺序访问,而 RAM 又分为动态和静态两种,静态 RAM 由于集成度较低,一般容量小,速度快,而动态 RAM 集成度较高,主要通过给电容充电和放电实现,速度没有静态 RAM 快,所以一般将动态 RAM 做为主存,而静态 RAM 作为 CPU 和主存之间的高速缓存 (cache),用来屏蔽 CPU 和主存速度上的差异,也就是我们经常看到的 L1 , L2 缓存。每一级别缓存速度变低,容量变大。

CPU 的指令执行

为什么可以流水线/乱序执行?因为cpu 内部部件很多,负责不同的指令 ==> 不同指令可以并行执行。

取址,译码,执行,这是一个指令的执行过程,所有指令都会严格按照这个顺序执行,但是多个指令之间其实是可以并行的,对于单核 CPU 来说,同一时刻只能有一条指令能够占有执行单元运行,这里说的执行是 CPU 指令处理 (取指,译码,执行) 三步骤中的第三步,也就是运算单元的计算任务,所以为了提升 CPU 的指令处理速度,所以需要保证运算单元在执行前的准备工作都完成,这样运算单元就可以一直处于运算中,而刚刚的串行流程中,取指,解码的时候运算单元是空闲的,而且取指和解码如果没有命中高速缓存还需要从主存取,而主存的速度和 CPU 不在一个级别上,所以指令流水线 可以大大提高 CPU 的处理速度

缓存速度的差异

从CPU到 大约需要的 CPU 周期 大约需要的时间
主存   约60-80纳秒
QPI 总线传输(between sockets, not drawn)   约20ns
L3 cache 约40-45 cycles, 约15ns
L2 cache 约10 cycles, 约3ns
L1 cache 约3-4 cycles, 约1ns
寄存器 1 cycle  

当CPU执行运算的时候,它先去L1查找所需的数据,再去L2,然后是L3,最后如果这些缓存中都没有,所需的数据就要去主内存拿。走得越远,运算耗费的时间就越长。如果你的目标是让端到端的延迟只有 10毫秒,而其中花80纳秒去主存拿一些未命中数据的过程将占很重的一块。如果你在做一些很频繁的事,你要确保数据在L1缓存中

缓存的存取——缓存行

高性能队列——Disruptor

cpu和内存的速度差异 ==> 缓存 ==> 多级缓存 ==> Cache是由很多个cache line组成的。每个cache line通常是64字节,并且它有效地引用主内存中的一块儿地址。CPU每次从主存中拉取数据时,会把相邻的数据也存入同一个cache line。也就是说,假设一个cache line 对应内存地址是0x1000,存着一个volatile变量,你改了这个变量,那么跟它挨着的另一个变量(地址为0x1008)也会失效(假设它们同属于一个java对象内存结构,或都是某个数组的元素)因为整个cache line被标记为失效了。下次访问第二个变量时,便需要从内存中加载到缓存,再加载到cpu。从某种程度上可以说:cpu一直是批量访问缓存/内存的

因此,缓存行中的64byte 数据,一个失效全失效,有时会带来一些性能问题。

JVM4——《深入拆解java 虚拟机》笔记 2018年07月20日因为 缓存行,jvm 使用了字段内存对齐机制。

volatile 字段和缓存行也有一番故事

多级缓存带来的问题

  1. 所谓线程 安全,最后可以归结到 并发读写 问题。参见 多线程
  2. 所谓数据结构,最后可以归结到 读写效率的 权衡问题。 参见hbase 泛谈 数据信息 和 结构信息(提高读写效率)混在一起,因为磁盘的缘故, 顺序写即可提高读效率。而查询/读效率 的提高花活儿就比较多了,并且通常 会降低写效率。

基于共享内存的数据通信问题从某种视角来看,共享内存模型下的并发问题都是“共享内存,但不共享寄存器/L1/L2缓存”导致的。

多级缓存问题的部分解决——内存屏障

Compiler 和 cpu 经常搞一些 optimizations,这种单线程视角下的优化在多线程环境下是不合时宜的,为此要用 memory barriers 来禁止 Compiler 和 cpu 搞这些小动作。 For purposes here, I assume that the compiler and the hardware don’t introduce funky optimizations (such as eliminating some “redundant” variable reads, a valid optimization under a single-thread assumption).

老司机谈技术天花板——做自己的破壁人

硬件为了加快速度,会弄各种缓存,然后就有一个缓存/副本一致性问题,也会定一些一致性规则(什么时候同步之类)。但基本就是,你不明确要求,硬件就当缓存是有效的。那么就有了打掉缓存的指令(即强制同步),然后编译器/runtime 支持该指令,最终反映在语言层面上。

Java和操作系统交互细节插入内存屏障的指令,会根据指令类型不同有不同的效果,例如在 monitorexit 释放锁后会强制刷新缓存,而 volatile 对应的内存屏障会在每次写入后强制刷新到主存,并且由于 volatile 字段的特性,编译器无法将其分配到寄存器,所以每次都是从主存读取,所以 volatile 适用于读多写少得场景

资源的有限性和需求的无限性

  计算能力 需求   备注
软硬件 cpu 创建线程的业务是无限的 用一个数据结构 表示和存放你要执行的任务/线程/进程,任尓干着急,我调度系统按既有的节奏来。  
java线程池 线程池管理的线程 要干的活儿是无限的 用一个runnable对象表示一个任务,线程池线程依次从队列中取出任务来执行 线程池管理的线程数可以扩大和缩小
goroutine goroutine调度器管理的线程 要干的活儿是无限的 用协程表示一个任务,线程从各自的队列里取出任务执行 A线程干完了,还可以偷B线程队列的活儿干

为什么会有人觉得优化没有必要,因为他们不理解有多耗时

Teach Yourself Programming in Ten Years

Remember that there is a “computer” in “computer science”. Know how long it takes your computer to execute an instruction, fetch a word from memory (with and without a cache miss), read consecutive words from disk, and seek to a new location on disk.

Approximate timing for various operations on a typical PC:

  耗时
execute typical instruction 1/1,000,000,000 sec = 1 nanosec
fetch from L1 cache memory 0.5 nanosec
branch misprediction 5 nanosec
fetch from L2 cache memory 7 nanosec
Mutex lock/unlock 25 nanosec
fetch from main memory 100 nanosec
send 2K bytes over 1Gbps network 20,000 nanosec
read 1MB sequentially from memory 250,000 nanosec
fetch from new disk location (seek) 8,000,000 nanosec
read 1MB sequentially from disk 20,000,000 nanosec
send packet US to Europe and back 150 milliseconds = 150,000,000 nanosec
上下文切换 数千个CPU时钟周期,1微秒

线程切换的成本

不仅创建线程的代价高,线程切换的开销也很高

  1. 线程切换只能在内核态完成,如果当前用户处于用户态,则必然引起用户态与内核态的切换。
  2. 上下文切换,前面说线程信息需要用一个task_struct保存,线程切换时,必然需要将旧线程的task_struct从内核切出,将新线程的切入,带来上下文切换。除此之外,还需要切换寄存器、程序计数器、线程栈(包括操作栈、数据栈)等。2019.03.22补充:《反应式设计模式》尽管CPU已经越来越快,但更多的内部状态已经抵消了纯执行速度上带来的进步,使得上下文切换大约需要耗费1微秒的时间(数千个CPU时钟周期),这一点在二十年来几乎没有大的改进。
  3. 执行线程调度算法,线程1放弃cpu ==> 线程调度算法找下一个线程 ==> 线程2
  4. 缓存缺失,切换线程,需要执行新逻辑。如果二者的访问的地址空间不相近,则会引起缓存缺失。 PS “进程切换”的代价更大巨大,linux线程切换和进程切换当你改变虚拟内存空间的时候,处理的页表缓冲(processor’s Translation Lookaside Buffer (TLB))或者相当的神马东西会被全部刷新,这将导致内存的访问在一段时间内相当的低效。

用户态与内核态切换有什么代价呢?

Understanding User and Kernel Mode

  1. 在操作系统中,In Kernel mode, the executing code has complete and unrestricted access to the underlying hardware. It can execute any CPU instruction and reference any memory address. 而用户态可以访问的指令和地址空间是受限的
  2. 用户态和内核态的切换通常涉及到内存的复制,比如内核态read 得到的数据返回给 用户态,因为用户态访问不了内核态的read 返回数据。
  3. jvm 则再插一腿,因为jvm 数据不仅在用户态,jvm 还希望数据是由jvm heap管理,所以对于read 操作来讲,数据从内核态 ==> 用户态 ==> jvm heap 经历了两次复制,netty 中允许使用堆外内存(对于linux来说,jvm heap和堆外内存都在进程的堆内存之内) 减少一次复制
  4. linux 和jvm 都可以使用 mmap来减少用户态和内核态的内存复制,但一是应用场景有限,二是代码复杂度提升了好多。

单核CPU技术瓶颈 ==> CPU 向多核发展 ==> 多台服务器

2019.3.28 补充

  1. 语言层面,golang协程、java9 支持反应式等
  2. 架构层面,全异步化、反应式架构、分布式计算

通过语言层、框架层提出新的模型,引导写出并行度高的代码