开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

基于Go技术栈的微服务构建

发表于 2017-11-22

在大型系统的微服务化构建中,一个系统会被拆分成许多模块。这些模块负责不同的功能,组合成系统,最终可以提供丰富的功能。在这种构建形式中,开发者一般会聚焦于最大程度解耦模块的功能以减少模块间耦合带来的额外开发成本。同时,微服务面临着如何部署这些大量的服务系统、如何运维这些系统等新问题。

本文的素材来源于我们在开发中的一些最佳实践案例,从开发、监控、日志等角度介绍了一些我们基于Go技术栈的微服务构建经验。

开 发

微服务的开发过程中,不同模块由不同的开发者负责,明确定义的接口有助于确定开发者的工作任务。最终的系统中,一个业务请求可能会涉及到多次接口调用,如何准确清晰的调用远端接口,这也是一大挑战。对于这些问题,我们使用了gRPC来负责协议的制订和调用。

传统的微服务通常基于http协议来进行模块间的调用,而在我们的微服务构建中,选用了Google推出的gRPC框架来进行调用。下面这张简表比较了http rpc框架与gRPC的特性:


gRPC的接口需要使用Protobuf3定义,通过静态编译后才能成功调用。这一特性减少了由于接口改变带来的沟通成本。如果使用http rpc,接口改变就需要先改接口文档,然后周知到调用者,如果调用者没有及时修改,很可能会到服务运行时才能发现错误。而gRPC的这种模式,接口变动引起的错误保证在编译时期就能消除。

在性能方面,gRPC相比传统的http rpc协议有非常大的改善(根据这个评测,gRPC要快10倍)。gRPC使用http 2协议进行传输,相比较http 1.1, http 2复用tcp连接,减少了每次请求建立tcp连接的开销。需要指出的是,如果单纯追求性能,之前业界一般会选用构建在tcp协议上的rpc协议(thrift等),但四层协议无法方便的做一些传输控制。相比而言,gRPC可以在http header中放入控制字段,配合nginx等代理服务器,可以很方便的实现转发/灰度等功能。

接下来着重谈谈我们在实践中如何使用gRPC的一些特性来简化相关开发流程。

1. 使用context来控制请求的生命周期

在gRPC的go语言实现中,每个rpc请求的第一个参数都是context。http2协议会将context放在HEADER中,随着链路传递下去,因此可以为每个请求设置过期时间,一旦遇到超时的情况,发起方就会结束等待,返回错误。

ctx := context.Background() // blank context

ctx, cancel = context.WithTimeout(ctx, 5*time.Second)

defer cancel( )

grpc.CallServiveX(ctx, arg1)

上述这段代码,发起方设置了大约5s的等待时间,只要远端的调用在5s内没有返回,发起方就会报错。

除了能加入超时时间,context还能加入其他内容,下文我们还会见到context的另一个妙用。

2.使用TLS实现访问权限控制

gRPC集成了TLS证书功能,为我们提供了很完善的权限控制方案。在实践中,假设我们的系统中存在服务A,由于它负责操作用户的敏感内容,因此需要保证A不被系统内的其他服务滥用。为了避免滥用,我们设计了一套自签名的二级证书系统,服务A掌握了自签名的根证书,同时为每个调用A的服务颁发一个二级证书。这样,所有调用A的服务必须经过A的授权,A也可以鉴别每个请求的调用方,这样可以很方便的做一些记录日志、流量控制等操作。

3. 使用trace在线追踪请求

gRPC内置了一套追踪请求的trace系统,既可以追踪最近10个请求的详细日志信息,也可以记录所有请求的统计信息。

当我们为请求加入了trace日志后,trace系统会为我们记录下最近10个请求的日志,下图中所示的例子就是在trace日志中加入了对业务数据的追踪。


在宏观上,trace系统为我们记录下请求的统计信息,比如请求数目、按照不同请求时间统计的分布等。


需要说明的是,这套系统暴露了一个http服务,我们可以通过debug开关在运行时按需打开或者关闭,以减少资源消耗。

监控

1.确定监控指标

在接到为整个系统搭建监控系统这个任务时,我们面对的第一个问题是要监控什么内容。针对这个问题,GoogleSRE这本书提供了很详细的回答,我们可以监控四大黄金指标,分别是延时、流量、错误和饱和度。

  • 延时衡量了请求花费的时间。需要注意的,考虑到长尾效应,使用平均延时作为延时方面的单一指标是远远不够的。相应的,我们需要延时的中位数90%、95%、99%值来帮助我们了解延时的分布,有一种更好的办法是使用直方图来统计延时分布。
  • 流量衡量了服务面临的请求压力。针对每个API的流量统计能让我们知道系统的热点路径,帮助优化。
  • 错误监控是指对错误的请求结果的统计。同样的,每个请求有不同的错误码,我们需要针对不同的错误码进行统计。配合上告警系统,这类监控能让我们尽早感知错误,进行干预。
  • 饱和度主要指对系统CPU和内存的负载监控。这类监控能为我们的扩容决策提供依据。

2.监控选型

选择监控方案时,我们面临的选择主要有两个,一是公司自建的监控系统,二是使用开源Prometheus系统搭建。这两个系统的区别列在下表中。


考虑到我们的整个系统大约有100个容器分布在30台虚拟机上,Prometheus的单机存储对我们并不是瓶颈。我们不需要完整保留历史数据,自建系统的最大优势也不足以吸引我们使用。相反,由于希望能够统计四大黄金指标延生出的诸多指标,Prometheus方便的DSL能够很大程度上简化我们的指标设计。

最终,我们选择了Prometheus搭建监控系统。整个监控系统的框架如下图所示。


各服务将自己的地址注册到consul中,Prometheus会自动从consul中拉取需要监控的目标地址,然后从这些服务中拉取监控数据,存放到本地存储中。在Prometheus自带的Web UI中可以快捷的使用PromQL查询语句获取统计信息,同时,还可以将查询语句输入grafana,固定监控指标用于监控。


此外,配合插件AlertManager,我们能够编写告警规则,当系统出现异常时,将告警发送到手机/邮件/信箱。

日志

1.日志格式

一个经常被忽略的问题是如何选择日志记录的格式。良好的日志格式有利于后续工具对日志内容的切割,便于日志存储的索引。我们使用logrus来打印日志到文件,logrus工具支持的日志格式包裹以空格分隔的单行文本格式、json格式等等。

文本格式

time=”2015-03-26T01:27:38-04:00″ level=debug g=”Started observing beach” animal=walrus number=8

time=”2015-03-26T01:27:38-04:00″ level=info msg=”A group of walrus emerges from the ocean” animal=walrus size=10Json格式

{“animal”:”walrus”,”level”:”info”,”msg”:”A group of walrus emerges from theocean”,”size”:10,”time”:”2014-03-10 19:57:38.562264131 -0400 EDT”}

{“level”:”warning”,”msg”:”The group’s number increased tremendously!”,”number”:122,”omg”:true,”time”:”2014-03-10 19:57:38.562471297 -0400 EDT”}

2.端到端链路上的调用日志收集

在微服务架构中,一个业务请求会经历多个服务,收集端到端链路上的日志能够帮助我们判断错误发生的具体位置。在这个系统中,我们在请求入口处,生成了全局ID,通过gRPC中的context将ID在链路中传递。将不同服务的日志收集到graylog中,查询时就能通过一个ID,将整个链路上的日志查询出来。


上图中,使用session-id来作为整个调用链的ID可以进行全链路检索。

小结

微服务构建的系统中,在部署、调度、服务发现、一致性等其他方面都有挑战,Go技术栈在这些方面都有最佳实践(docker,k8s,consul,etcd等等)。具体内容在网上已经有很完善的教程,在此不用班门弄斧,有需要的可以自行查阅。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

java8 lambda加强版 StreamEx

发表于 2017-11-22

Contents

  • Glossary
  • Stream sources
  • New intermediate operations
  • filtering
  • mapping
  • flat-mapping
  • distinct
  • sorting
  • partial reduction
  • concatenate
  • peek
  • misc
  • New terminal operations
  • Collector shortcuts
  • Search
  • Folding
  • Primitive operations
  • forEach-like operations
  • misc
  • Collectors
  • Basic collectors
  • Adaptor collectors

Glossary

  • any: either of StreamEx, IntStreamEx, LongStreamEx, DoubleStreamEx, EntryStream, sometimes without EntryStream.
  • element: any object or primitive value which appears in the stream pipeline.
  • entry: a pair of objects usually (but not always) having the Map.Entry semantics. The first object in the pair is key, the second one is value.

Stream sources

What I want How to get it
Empty Stream any.empty()
Stream of array, varargs, Collection, Spliterator, Iterator, Stream any.of()
Stream of Enumeration StreamEx.of()
Stream of array or List elements in reverse order StreamEx.ofReversed()
Stream of boxed Collection<Integer>, etc. with unboxing IntStreamEx/LongStreamEx/DoubleStreamEx.of()
Stream of boxed array Integer[], etc. with unboxing IntStreamEx/LongStreamEx/DoubleStreamEx.of()
Stream of byte[], char[], short[] array IntStreamEx.of()
Stream of float[] array DoubleStreamEx.of()
Infinite Stream from Supplier any.generate()
Infinite Stream using iterative function any.iterate()
Convert three-argument for loop to Stream any.iterate()
Fixed length Stream of constant elements any.constant()
Stream from mutable object which is left in the known state after full Stream consumption any.produce()
Custom stream source which maintains mutable state Emitter/IntEmitter/LongEmitter/DoubleEmitter
Stream of array or List with indices EntryStream.of()
Stream of single value or empty if null supplied StreamEx.ofNullable()
Stream of Map keys (with optional values filter) StreamEx.ofKeys()
Stream of Map values (with optional keys filter) StreamEx.ofValues()
Stream of Map entries or explicit key-value pairs EntryStream.of()
Zip two arrays or lists any.zip()
Split CharSequence with regexp StreamEx.split()
Stream of List subLists of fixed length StreamEx.ofSubLists()
Stream of all elements of tree-like structure StreamEx.ofTree()
Stream of all elements of tree-like structure tracking the elements depth EntryStream.ofTree()
Stream of all possible pairs of array or List elements StreamEx/EntryStream.ofPairs()
Stream of all possible tuples of given length of Collection elements StreamEx.cartesianPower()
Stream of all possible tuples of given Collection of collections StreamEx.cartesianProduct()
Stream of permutations StreamEx.ofPermutations()
Stream of combinations StreamEx.ofCombinations()
Stream of array or List indices (with optional element filter) IntStreamEx.ofIndices()
Stream of range of integral values (with optional step parameter) IntStreamEx/LongStreamEx.range()/rangeClosed()
Stream of increasing int or long values IntStreamEx.ints()/LongStreamEx.longs()
Stream of random numbers IntStreamEx/LongStreamEx/DoubleStreamEx.of(Random, ...)
Stream of CharSequence symbols IntStreamEx.ofChars()/ofCodePoints()
Stream of BitSet true bits IntStreamEx.of(BitSet)
Stream of lines from file or Reader StreamEx.ofLines()
Stream of bytes from the InputStream IntStreamEx.of(InputStream)
Stream of ints from the IntBuffer IntStreamEx.of(IntBuffer)
Stream of longs from the LongBuffer LongStreamEx.of(LongBuffer)
Stream of doubles from the DoubleBuffer DoubleStreamEx.of(DoubleBuffer)

New intermediate operations

filtering

What I want How to get it
Remove nulls StreamEx/EntryStream.nonNull()
Remove entries which keys or values are null EntryStream.nonNullKeys()/nonNullValues()
Remove elements by predicate any.remove()
Remove given elements StreamEx/IntStreamEx/LongStreamEx.without()
Remove by value extracted by supplied mapper function StreamEx.removeBy()
Leave only elements greater/less/at least/at most given value IntStreamEx/LongStreamEx/DoubleStreamEx.greater()/atLeast()/less()/atMost()
Filter by value extracted by supplied mapper function StreamEx.filterBy()
Filter entries which keys or values satisfy the predicate EntryStream.filterKeys()/filterValues()
Filter entries applying the BiPredicate to key and value EntryStream.filterKeyValue()
Remove entries which keys or values satisfy the predicate EntryStream.removeKeys()/removeValues()
Remove entries applying the BiPredicate to key and value EntryStream.removeKeyValue()
Select elements which are instances of given class StreamEx.select()
Select entries which keys or values are instances of given class EntryStream.selectKeys()/selectValues()
Take stream elements while the condition is true any.takeWhile()
Take stream elements while the condition is true including first violating element any.takeWhileInclusive()
Skip stream elements while the condition is true any.dropWhile()

mapping

What I want How to get it
Map array or List indices to the corresponding elements IntStreamEx.elements()
Map element to the entry any.mapToEntry()
Map entry keys leaving values unchanged EntryStream.mapKeys()/mapToKey()
Map entry values leaving keys unchanged EntryStream.mapValues()/mapToValue()
Map entry key and value using BiFunction EntryStream.mapKeyValue()
Swap entry key and value (so entry value becomes key and vice versa) EntryStream.invert()
Drop entry values leaving only keys EntryStream.keys()
Drop entry keys leaving only values EntryStream.values()
Convert every entry to String EntryStream.join()
Map pair of adjacent elements to the single element any.pairMap()
Map only first or last element, leaving others as is any.mapFirst()/mapLast()
Map stream element providing special mapper function for the first or last element any.mapFirstOrElse()/mapLastOrElse()
Attach the first stream element to every stream element StreamEx.withFirst()

flat-mapping

What I want How to get it
Flat-map primitive stream to the stream of other type IntStreamEx/LongStreamEx/DoubleStreamEx. flatMapToInt()/flatMapToLong()/flatMapToDouble()/flatMapToObj()
Flatten multiple collections to the stream of their elements StreamEx/EntryStream.flatCollection()
Flatten multiple arrays to the stream of their elements StreamEx/EntryStream.flatArray()
Flatten multiple maps to the stream of their entries StreamEx.flatMapToEntry()
Perform cross product of current stream with given array, Collection or Stream source creating entries StreamEx.cross()
Flat-map entry keys leaving values unchanged EntryStream.flatMapKeys()/flatMapToKey()
Flat-map entry values leaving keys unchanged EntryStream.flatMapValues()/flatMapToValue()
Flat-map entry key and value using BiFunction EntryStream.flatMapKeyValue()

distinct

What I want How to get it
Leave only distinct elements which appear at least given number of times StreamEx.distinct(atLeast)
Leave distinct elements using custom key extractor StreamEx/EntryStream.distinct(keyExtractor)
Leave only entries having distinct keys EntryStream.distinctKeys()
Leave only entries having distinct values EntryStream.distinctValues()

sorting

What I want How to get it
Sort in reverse order any.reverseSorted()
Sort using given key any.sortedBy()/sortedByInt()/sortedByLong()/sortedByDouble()

partial reduction

Partial reduction is a procedure of combining several adjacent stream elements. Usually BiPredicate is used to determine which elements should be combined. BiPredicate is applied to the pair of adjacent elements and returns
true if they should be combined.

What I want How to get it
Group some adjacent stream elements into List StreamEx.groupRuns()
Reduce some adjacent stream elements using Collector or BinaryOperator StreamEx.collapse()
Collapse some adjacent stream elements into interval StreamEx.intervalMap()
Remove adjacent duplicate elements counting them StreamEx.runLengths()
Group adjacent entries with equal keys EntryStream.collapseKeys()

concatenate

What I want How to get it
Add new elements/Collection/Stream to the end of existing stream any.append()
Add new elements/Collection/Stream to the beginning of existing stream any.prepend()

peek

What I want How to get it
Peek only entry keys EntryStream.peekKeys()
Peek only entry values EntryStream.peekValues()
Peek entry keys and values using BiConsumer EntryStream.peekKeyValue()
Peek only first or last stream element any.peekFirst()/peekLast()

misc intermediate operations

What I want How to get it
Extract first stream element and use it to alternate the rest of the stream StreamEx.headTail()
Define almost any custom intermediate operation recursively StreamEx.headTail()
Execute custom-defined operation in fluent manner any.chain()
Work-around parallel stream skip bug prior to Java 8u60 any.skipOrdered()
Perform parallel stream computation using the custom ForkJoinPool any.parallel(pool)
Zip two streams together StreamEx.zipWith()
Get the stream of cumulative prefixes any.prefix()/EntryStream.prefixKeys()/EntryStream.prefixValues()
Intersperse the stream with given delimiters any.intersperse()
Replace the stream contents if the stream is empty StreamEx.ifEmpty()/EntryStream.ifEmpty()

New terminal operations

Collector shortcuts

What I want How to get it
Collect elements to List, Set or custom Collection StreamEx/EntryStream.toList()/toSet()/toCollection()
Collect elements to List or Set adding custom final step StreamEx/EntryStream.toListAndThen()/toSetAndThen()
Collect elements to immutable List or Set StreamEx/EntryStream.toImmutableList()/toImmutableSet()
Collect elements or entries to Map StreamEx/EntryStream.toMap()/toSortedMap()/toNavigableMap()
Collect entries to immutable Map EntryStream.toImmutableMap()
Collect entries to Map adding custom final step EntryStream.toMapAndThen()
Collect entries to custom Map EntryStream.toCustomMap()
Partition elements using the Predicate StreamEx.partitioningBy()/partitioningTo()
Grouping elements StreamEx.groupingBy()/groupingTo()
Grouping entries EntryStream.grouping()/groupingTo()
Joining elements to String any.joining()
Flatten collections and collect them to single final collection StreamEx.toFlatList()/toFlatCollection()
Getting maximal element using custom key extractor any.maxBy()/maxByInt()/maxByLong()/maxByDouble()
Getting minimal element using custom key extractor any.minBy()/minByInt()/minByLong()/minByDouble()

Search

What I want How to get it
Check whether stream has given element StreamEx/EntryStream/IntStreamEx/LongStreamEx.has()
Find element satisfying the predicate any.findFirst(Predicate)/findAny(Predicate)
Find the index of element satisfying the predicate or equal to given value any.indexOf()

Folding

What I want How to get it
Fold elements left-to-right any.foldLeft()
Fold elements right-to-left StreamEx/EntryStream.foldRight()
Get List of cumulative prefixes or suffixes StreamEx/EntryStream.scanLeft()/scanRight()
Get primitive array of cumulative prefixes IntStreamEx/LongStreamEx/DoubleStreamEx.scanLeft()

Primitive operations

What I want How to get it
Collect IntStreamEx to byte[], char[] or short[] IntStreamEx.toByteArray()/toCharArray()/toShortArray()
Collect IntStreamEx to BitSet IntStreamEx.toBitSet()
Collect DoubleStreamEx to float[] DoubleStreamEx.toFloatArray()
Collect stream of chars or codepoints to String IntStreamEx.charsToString()/codePointsToString()

forEach-like operations

What I want How to get it
Perform operation on every adjacent pair of elements StreamEx.forPairs()
Perform operation on entry key and value using BiConsumer EntryStream.forKeyValue()

misc terminal operations

What I want How to get it
Convert IntStreamEx to InputStream IntStreamEx.asByteInputStream()
Drain stream content into the existing Collection StreamEx.into()
Drain stream content into the existing Map EntryStream.into()

Collectors

Basic collectors

What I want How to get it
Collect to array MoreCollectors.toArray()
Collect to boolean array using the Predicate applied to each element MoreCollectors.toBooleanArray()
Collect to EnumSet MoreCollectors.toEnumSet()
Count number of distinct elements using custom key extractor MoreCollectors.distinctCount()
Get the List of distinct elements using custom key extractor MoreCollectors.distinctBy()
Simply counting, but get the result as Integer MoreCollectors.countingInt()
Get the first or last element only MoreCollectors.first()/last()
Get the element only if there’s exactly one element MoreCollectors.onlyOne()
Get the given number of first or last elements in the List MoreCollectors.head()/tail()
Get the given number of greatest/least elements according to the given Comparator or natural order MoreCollectors.greatest()/least()
Get all the maximal or minimal elements according to the given Comparator or natural order MoreCollectors.maxAll()/minAll()
Get the index of maximal or minimal element according to the given Comparator or natural order MoreCollectors.minIndex()/maxIndex()
Get both maximal and minimal stream element according to the given Comparator MoreCollectors.minMax()
Get the intersection of input collections MoreCollectors.intersecting()
Get the result bitwise-and operation MoreCollectors.andingInt()/andingLong()
Join the elements into string with possible limit to the string length (adding ellipsis if necessary) Joining.with()
Perform a group-by with the specified keys domain, so every key is initialized even if absent in the input MoreCollectors.groupingBy()/groupingByEnum()
Partition input according to the Predicate MoreCollectors.partitioningBy()
Get the common prefix or common suffix String of input elements MoreCollectors.commonPrefix()/commonSuffix()
Get the list of input elements removing the elements which follow their dominator element MoreCollectors.dominators()

Adaptor collectors

What I want How to get it
Collect using two independent collectors MoreCollectors.pairing()
Filter the input before passing to the collector MoreCollectors.filtering()
Map the input before passing to the collector MoreCollectors.mapping()
Flat-map the input before passing to the collector MoreCollectors.flatMapping()
Perform a custom final operation after the collection finishes MoreCollectors.collectingAndThen()
Perform a downstream collection if all elements satisfy the predicate MoreCollectors.ifAllMatch()

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

JVM系列之Java内存结构详解

发表于 2017-11-22

Java内存结构详解

相信大多数Javaer对Java的内存结构都有一定的了解,但如果对于Java的内存结构只停留的”堆”,”栈”中显然是不够的。今天来给大家详细谈一谈Java的内存区域结构,本文基于 JDK7 的内存结构做讲解,JDK8的内存结构加上了metaspace,有些许变动,想详细了解的同学请自行翻阅相关资料。

文章结构

  1. 内存结构图
  2. 根据内存结构图各个区域做详细讲解

1 . 内存结构图

图片说明

  • 方法区,堆区(标绿)为所有线程共享的内存区域,虚拟机栈,本地方法栈,程序计数器(标蓝)为线程似有的内存区域,即线程隔离的。

2 . 各个区域详解

程序计数器

代码的运行是有顺序的,但当CPU在多线程间切换时,当从A线程切换到B线程,再切回到A线程时,CPU如何知道该从A线程的哪里继续执行呢?CPU工作时就是根据每个线程的程序计数器的值来选取下一条需要执行的字节码指令,即”找到它离开时的位置来继续执行”。需要提示的是,当CPU执行的是一个Java方法时,程序计数器记录的是正在执行的虚拟机字节码指令的地址。如果执行的是Native方法,这个计数器值为Undefined,即不发挥作用。

虚拟机栈

虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈和出栈的过程。
局部变量表存放了编译期间可以知道大小的各种类型变量,它所需要的内存空间大小在编译期间就已经分配,当一个方法被调用时,栈帧进入虚拟机栈,在运行期间,局部变量表大小是不会变化的。

本地方法栈

本地方法栈与虚拟机栈锁发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行Java方法服务,而本地方法栈为虚拟机执行Native方法服务。需要注意的是,由于虚拟机规范对于本地方法栈的具体实现没有做强制要求,所以Sun HotSpot直接把本地方法栈和虚拟机栈合二为一。

Java堆

Java堆是Javaer需要重点关注的一块区域,因为涉及到内存的分配(new关键字,反射等)与回收(回收算法,收集器等),这里不做太多详细的介绍内存的分配与回收,后期有时间专门出博客讲解。在Java虚拟机规范中的描述是:所有对象实例以及数组都要在堆上分配。需要特别注意的是,线程共享的Java堆中可能分出多个线程私有的分配缓冲区(TLAB,这是为了并发分配内存时的脏分配问题,需要使用相关参数来开启。虚拟机默认使用CAS加上失败重试机制解决脏分配问题)。此外,Java堆在HotSpot中的实现是可扩展的。参数-Xmx/-Xms来控制。

方法区(永久代)

方法区用于存储已经被虚拟机加载的类信息,常量(“zdy”,”123”等),静态变量(static变量)等数据。方法区有一个别名叫永久代(Permanent Generation),这是因为HotSpot设计团队把GC分代收集扩展至方法区,这样HotSpot的垃圾收集器可以像管理Java堆一样管理这部分内存,能够省钱专门为方法区编写内存管理代码的工作。对于其他虚拟机(J9)等,是没有永久代这个概念的。
永久代的配置参数: -XX:MaxPermSize

运行时常量池

运行时常量池是方法区的一部分,用于存放编译期生成的各种字面量(“zdy”,”123”等)和符号引用。运行时常量池具有动态性,并非只有Class文件中的内容才能进入运行时常量池,运行期间也能将新的常量放入池中。如String.intern()方法。

附加–直接内存

直接内存不是Java虚拟机规范的内存区域。但是这部分也被Javaer频繁使用,而且也会导致OutOfMember异常。所以这里顺带提一提。
在JDK4中加入的NIO,使用了Native函数库直接分配堆外内存,然后通过一个缓存在Java堆中的Buffer来指向这块地址进行操作。这样能够避免Java堆和Native堆来回复制数据,在某些场景可以显著提高性能。
直接内存不受任何虚拟机参数控制,但很明显,你不能大于物理内存大小。

结语

JDK7的内存模型就大致介绍完了,JVM系列博客后期将带给大家更加深入的内容,下期预告:JVM系列之实战内存溢出异常

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

视频笔记:如何正确使用 Golang Context 视频信

发表于 2017-11-22
  • 视频信息
  • 自我介绍
  • 为什么需要 Context
  • Context 实现细节
    • 示例 Context 链
    • context.Context API
    • 什么时候应该使用 Context?
    • 如何创建 Context?
    • 如何集成到 API 里?
    • Context 放哪?
    • Context 包的注意事项
    • 终止请求 (Request Cancellation)
    • Context.Value - Request 范畴的值
      • context.Value API 的万金油(duct tape)
      • 约束 key 的空间
      • Context.Value 是 immutable 的
      • 应该把什么放到 Context.Value 里?
      • 那么用 Context.Value 有什么问题?
      • 那什么可以放到 Context.Value 里去?
      • 例子
      • 人们滥用 Context.Value 的原因
    • 总结 Context.Value
  • 总结 Context
  • Q&A
    • 数据库的访问也用 Context 么?

视频信息

How to correctly use package context
by Jack Lindamood
at Golang UK Conf. 2017

  • 视频:www.youtube.com/watch?v=-_B…
  • 博文:medium.com/@cep21/how-…

自我介绍

Jack Lindamood Github: @cep21, Medium: @cep21, email: cep221 at gmail.com

  • 已经写了 4 年 Go 了
  • 目前是 Twitch 的软件工程师
  • Twitch 的主要后端都是用 Go 写的
  • 有几百个项目 repo

为什么需要 Context

  • 每一个长请求都应该有个超时限制
  • 需要在调用中传递这个超时
    • 比如开始处理请求的时候我们说是 3 秒钟超时
    • 那么在函数调用中间,这个超时还剩多少时间了?
    • 需要在什么地方存储这个信息,这样请求处理中间可以停止

如果进一步考虑。

rpc fails

如上图这样的 RPC 调用,开始调用 RPC 1 后,里面分别调用了 RPC 2, RPC 3, RPC 4,等所有 RPC 调用成功后,返回结果。

这是正常的方式,但是如果 RPC 2 调用失败了会发生什么?

rpc fails

RPC 2 失败后,如果没有 Context 的存在,那么我们可能依旧会等所有的 RPC 执行完毕,但是由于 RPC 2 失败了,所以其实其它的 RPC 结果意义不大了,我们依旧需要给用户返回错误。因此我们白白的浪费了 10ms,完全没必要去等待其它 RPC 执行完毕。

那如果我们在 RPC 2 失败后,就直接给用户返回失败呢?

rpc fails

用户是在 30ms 的位置收到了错误消息,可是 RPC 3 和 RPC 4 依然在没意义的运行,还在浪费计算和IO资源。

rpc fails

所以理想状态应该是如上图,当 RPC 2 出错后,除了返回用户错误信息外,我们也应该有某种方式可以通知 RPC 3 和 RPC 4,让他们也停止运行,不再浪费资源。

所以解决方案就是:

  • 用信号的方式来通知请求该停了
  • 包含一些关于什么时间请求可能会结束的提示(超时)
  • 用 channel 来通知请求结束了

那干脆让我们把变量也扔那吧。😈

  • 在 Go 中没有线程/go routine 变量
    • 其实挺合理的,因为这样就会让 goroutine 互相产生依赖
  • 非常容易被滥用

Context 实现细节

context.Context:

  • 是不可变的(immutable)树节点
  • Cancel 一个节点,会连带 Cancel 其所有子节点 (从上到下)
  • Context values 是一个节点
  • Value 查找是回溯树的方式 (从下到上)

示例 Context 链

完整代码:play.golang.org/p/ddpofBV1Q…

1
2
3
4
5
6
7
8
9
maxima复制代码package main
func tree() {
ctx1 := context.Background()
ctx2, _ := context.WithCancel(ctx1)
ctx3, _ := context.WithTimeout(ctx2, time.Second * 5)
ctx4, _ := context.WithTimeout(ctx3, time.Second * 3)
ctx5, _ := context.WithTimeout(ctx3, time.Second * 6)
ctx6 := context.WithValue(ctx5, "userID", 12)
}

如果这样构成的 Context 链,其形如下图:

context chain

那么当 3 秒超时到了时候:

context chain

可以看到 ctx4 超时退出了。

当 5秒钟 超时到达时:

context chain

可以看到,不仅仅 ctx3 退出了,其所有子节点,比如 ctx5 和 ctx6 也都退出了。

context.Context API

基本上是两类操作:

  • 3个函数用于限定什么时候你的子节点退出;
  • 1个函数用于设置请求范畴的变量
1
2
3
4
5
6
7
8
reasonml复制代码type Context interface {
// 啥时候退出
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
// 设置变量
Value(key interface{}) interface{}
}

什么时候应该使用 Context?

  • 每一个 RPC 调用都应该有超时退出的能力,这是比较合理的 API 设计
  • 不仅仅 是超时,你还需要有能力去结束那些不再需要操作的行为
  • context.Context 是 Go 标准的解决方案
  • 任何函数可能被阻塞,或者需要很长时间来完成的,都应该有个 context.Context

如何创建 Context?

  • 在 RPC 开始的时候,使用 context.Background()
    • 有些人把在 main() 里记录一个 context.Background(),然后把这个放到服务器的某个变量里,然后请求来了后从这个变量里继承 context。这么做是不对的。直接每个请求,源自自己的 context.Background() 即可。
  • 如果你没有 context,却需要调用一个 context 的函数的话,用 context.TODO()
  • 如果某步操作需要自己的超时设置的话,给它一个独立的 sub-context(如前面的例子)

如何集成到 API 里?

  • 如果有 Context,将其作为第一个变量。
    • 如 func (d* Dialer) DialContext(ctx context.Context, network, address string) (Conn, error)
    • 有些人把 context 放到中间的某个变量里去,这很不合习惯,不要那么做,放到第一个去。
  • 将其作为可选的方式,用 request 结构体方式。
    • 如:func (r *Request) WithContext(ctx context.Context) *Request
  • Context 的变量名请用 ctx(不要起一些诡异的名字😓)

Context 放哪?

  • 把 Context 想象为一条河流流过你的程序(另一个意思就是说不要喝河里的水……🙊)
  • 理想情况下,Context 存在于调用栈(Call Stack) 中
  • 不要把 Context 存储到一个 struct 里
    • 除非你使用的是像 http.Request 中的 request 结构体的方式
  • request 结构体应该以 Request 结束为生命终止
  • 当 RPC 请求处理结束后,应该去掉对 Context 变量的引用(Unreference)
  • Request 结束,Context 就应该结束。(这俩是一对儿,不求同年同月同日生,但求同年同月同日死……💕)

Context 包的注意事项

  • 要养成关闭 Context 的习惯
    • 特别是 超时的 Contexts
  • 如果一个 context 被 GC 而不是 cancel 了,那一般是你做错了
1
2
livecodeserver复制代码ctx, cancel := context.WithTimeout(parentCtx, time.Second * 2)
defer cancel()
  • 使用 Timeout 会导致内部使用 time.AfterFunc,从而会导致 context 在计时器到时之前都不会被垃圾回收。
  • 在建立之后,立即 defer cancel() 是一个好习惯。

终止请求 (Request Cancellation)

当你不再关心接下来获取的结果的时候,有可能会 Cancel 一个 Context?

以 golang.org/x/sync/errgroup 为例,errgroup 使用 Context 来提供 RPC 的终止行为。

1
2
3
4
5
6
elm复制代码type Group struct {
cancel func()
wg sync.WaitGroup
errOnce sync.Once
err error
}

创建一个 group 和 context:

1
2
3
4
autoit复制代码func WithContext(ctx context.Context) (*Group, context.Context) {
ctx, cancel := context.WithCancel(ctx)
return &Group{cancel: cancel}, ctx
}

这样就返回了一个可以被提前 cancel 的 group。

而调用的时候,并不是直接调用 go func(),而是调用 Go(),将函数作为参数传进去,用高阶函数的形式来调用,其内部才是 go func() 开启 goroutine。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
go复制代码func (g *Group) Go(f func() error) {
g.wg.Add(1)
go func() {
defer g.wg.Done()
if err := f(); err != nil {
g.errOnce.Do(func() {
g.err = err
if g.cancel != nil {
g.cancel()
}
})
}
}()
}

当给入函数 f 返回错误,则使用 sync.Once 来 cancel context,而错误被保存于 g.err 之中,在随后的 Wait() 函数中返回。

1
2
3
4
5
6
7
stylus复制代码func (g *Group) Wait() error {
g.wg.Wait()
if g.cancel != nil {
g.cancel()
}
return g.err
}

注意:这里在 Wait() 结束后,调用了一次 cancel()。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
go复制代码package main
func DoTwoRequestsAtOnce(ctx context.Context) error {
eg, egCtx := errgroup.WithContext(ctx)
var resp1, resp2 *http.Response
f := func(loc string, respIn **http.Response) func() error {
return func() error {
reqCtx, cancel := context.WithTimeout(egCtx, time.Second)
defer cancel()
req, _ := http.NewRequest("GET", loc, nil)
var err error
*resp, err = http.DefaultClient.Do(req.WithContext(reqCtx))
if err == nil && (*respIn).StatusCode >= 500 {
return errors.New("unexpected!")
}
return err
}
}
eg.Go(f("http://localhost:8080/fast_request", &resp1))
eg.Go(f("http://localhost:8080/slow_request", &resp2))
return eg.Wait()
}

在这个例子中,同时发起了两个 RPC 调用,当任何一个调用超时或者出错后,会终止另一个 RPC 调用。这里就是利用前面讲到的 errgroup 来实现的,应对有很多并非请求,并需要集中处理超时、出错终止其它并发任务的时候,这个 pattern 使用起来很方便。

Context.Value - Request 范畴的值

context.Value API 的万金油(duct tape)

胶带(duct tape) 几乎可以修任何东西,从破箱子,到人的伤口,到汽车引擎,甚至到NASA登月任务中的阿波罗13号飞船(Yeah! True Story)。所以在西方文化里,胶带是个“万能”的东西。在中文里,恐怕万金油是更合适的对应词汇,从头疼、脑热,感冒发烧,到跌打损伤几乎无所不治。

当然,治标不治本,这点东西方文化中的潜台词都是一样的。这里提及的 context.Value 对于 API 而言,就是这类性质的东西,啥都可以干,但是治标不治本。

  • value 节点是 Context 链中的一个节点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fsharp复制代码package context
type valueCtx struct {
Context
key, val interface{}
}
func WithValue(parent Context, key, val interface{}) Context {
// ...
return &valueCtx{parent, key, val}
}
func (c *valueCtx) Value(key interface{}) interface{} {
if c.key == key {
return c.val
}
return c.Context.Value(key)
}

可以看到,WithValue() 实际上就是在 Context 树形结构中,增加一个节点罢了。

Context 是 immutable 的。

约束 key 的空间

为了防止树形结构中出现重复的键,建议约束键的空间。比如使用私有类型,然后用 GetXxx() 和 WithXxxx() 来操作私有实体。

1
2
3
4
5
6
7
8
9
10
11
go复制代码type privateCtxType string
var (
reqID = privateCtxType("req-id")
)
func GetRequestID(ctx context.Context) (int, bool) {
id, exists := ctx.Value(reqID).(int)
return id, exists
}
func WithRequestID(ctx context.Context, reqid int) context.Context {
return context.WithValue(ctx, reqID, reqid)
}

这里使用 WithXxx 而不是 SetXxx 也是因为 Context 实际上是 immutable 的,所以不是修改 Context 里某个值,而是产生新的 Context 带某个值。

Context.Value 是 immutable 的

再多次的强调 Context.Value 是 immutable 的也不过分。

  • context.Context 从设计上就是按照 immutable (不可变的)模式设计的
  • 同样,Context.Value 也是 immutable 的
  • 不要试图在 Context.Value 里存某个可变更的值,然后改变,期望别的 Context 可以看到这个改变
    • 更别指望着在 Context.Value 里存可变的值,最后多个 goroutine 并发访问没竞争冒险啥的,因为自始至终,就是按照不可变来设计的
    • 比如设置了超时,就别以为可以改变这个设置的超时值
  • 在使用 Context.Value 的时候,一定要记住这一点

应该把什么放到 Context.Value 里?

  • 应该保存 Request 范畴的值
    • 任何关于 Context 自身的都是 Request 范畴的(这俩同生共死)
    • 从 Request 数据衍生出来,并且随着 Request 的结束而终结

什么东西不属于 Request 范畴?

  • 在 Request 以外建立的,并且不随着 Request 改变而变化
    • 比如你 func main() 里建立的东西显然不属于 Request 范畴
  • 数据库连接
    • 如果 User ID 在连接里呢?(稍后会提及)
  • 全局 logger
    • 如果 logger 里需要有 User ID 呢?(稍后会提及)

那么用 Context.Value 有什么问题?

  • 不幸的是,好像所有东西都是由请求衍生出来的
  • 那么我们为什么还需要函数参数?然后干脆只来一个 Context 就完了?
1
2
3
reasonml复制代码func Add(ctx context.Context) int {
return ctx.Value("first").(int) + ctx.Value("second").(int)
}

曾经看到过一个 API,就是这种形式:

1
2
3
4
go复制代码func IsAdminUser(ctx context.Context) bool {
userID := GetUser(ctx)
return authSingleton.IsAdmin(userID)
}

这里API实现内部从 context 中取得 UserID,然后再进行权限判断。但是从函数签名看,则完全无法理解这个函数具体需要什么、以及做什么。

代码要以可读性为优先设计考虑。

别人拿到一个代码,一般不是掉进函数实现细节里去一行行的读代码,而是会先浏览一下函数接口。所以清晰的函数接口设计,会更加利于别人(或者是几个月后的你自己)理解这段代码。

一个良好的 API 设计,应该从函数签名就清晰的理解函数的逻辑。如果我们将上面的接口改为:

1
reasonml复制代码func IsAdminUser(ctx context.Context, userID string, authenticator auth.Service) bool

我们从这个函数签名就可以清楚的知道:

  • 这个函数很可能可以提前被 cancel
  • 这个函数需要 User ID
  • 这个函数需要一个authenticator来
  • 而且由于 authenticator 是传入参数,而不是依赖于隐式的某个东西,我们知道,测试的时候就很容易传入一个模拟认证函数来做测试
  • userID 是传入值,因此我们可以修改它,不用担心影响别的东西

所有这些信息,都是从函数签名得到的,而无需打开函数实现一行行去看。

那什么可以放到 Context.Value 里去?

现在知道 Context.Value 会让接口定义更加模糊,似乎不应该使用。那么又回到了原来的问题,到底什么可以放到 Context.Value 里去?换个角度去想,什么不是衍生于 Request?

  • Context.Value 应该是告知性质的东西,而不是控制性质的东西
  • 应该永远都不需要写进文档作为必须存在的输入数据
  • 如果你发现你的函数在某些 Context.Value 下无法正确工作,那就说明这个 Context.Value 里的信息不应该放在里面,而应该放在接口上。因为已经让接口太模糊了。

什么东西不是控制性质的东西?

  • Request ID
    • 只是给每个 RPC 调用一个 ID,而没有实际意义
    • 这就是个数字/字符串,反正你也不会用其作为逻辑判断
    • 一般也就是日志的时候需要记录一下
      • 而 logger 本身不是 Request 范畴,所以 logger 不应该在 Context 里
      • 非 Request 范畴的 logger 应该只是利用 Context 信息来修饰日志
  • User ID (如果仅仅是作为日志用)
  • Incoming Request ID

什么显然是控制性质的东西?

  • 数据库连接
    • 显然会非常严重的影响逻辑
    • 因此这应该在函数参数里,明确表示出来
  • 认证服务(Authentication)
    • 显然不同的认证服务导致的逻辑不同
    • 也应该放到函数参数里,明确表示出来

例子

调试性质的 Context.Value - net/http/httptrace

  • medium.com/@cep21/go-1…
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
go复制代码package main
func trace(req *http.Request, c *http.Client) {
trace := &httptrace.ClientTrace{
GotConn: func(connInfo httptrace.GotConnInfo) {
fmt.Println("Got Conn")
},
ConnectStart: func(network, addr string) {
fmt.Println("Dial Start")
},
ConnectDone: func(network, addr string, err error) {
fmt.Println("Dial done")
},
}
req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))
c.Do(req)
}
net/http 是怎么使用 httptrace 的?
  • 如果有 trace 存在的话,就执行 trace 回调函数
  • 这只是告知性质,而不是控制性质
    • http 不会因为存在 trace 与否就有不同的执行逻辑
    • 这里只是告知 API 的用户,帮助用户记录日志或者调试
    • 因此这里的 trace 是存在于 Context 里的
1
2
3
4
5
6
7
8
9
go复制代码package http
func (req *Request) write(w io.Writer, usingProxy bool, extraHeaders Header, waitForContinue func() bool) (err error) {
// ...
trace := httptrace.ContextClientTrace(req.Context())
// ...
if trace != nil && trace.WroteHeaders != nil {
trace.WroteHeaders()
}
}

回避依赖注入 - github.com/golang/oauth2

  • 这里比较诡异,使用 ctx.Value 来定位依赖
  • 不推荐这样做
    • 这里这样做基本上只是为了满足测试需求
1
2
3
4
5
6
7
8
go复制代码package main
import "github.com/golang/oauth2"
func oauth() {
c := &http.Client{Transport: &mockTransport{}}
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, c)
conf := &oauth2.Config{ /* ... */ }
conf.Exchange(ctx, "code")
}

人们滥用 Context.Value 的原因

  • 中间件的抽象
  • 很深的函数调用栈
  • 混乱的设计

context.Value 并没有让你的 API 更简洁,那是假象,相反,它让你的 API 定义更加模糊。

总结 Context.Value

  • 对于调试非常方便
  • 将必须的信息放入 Context.Value 中,会让接口定义更加不透明
  • 如果可以尽量明确定义在接口
  • 尽量不要用 Context.Value

总结 Context

  • 所有的长的、阻塞的操作都需要 Context
  • errgroup 是构架于 Context 之上很好的抽象
  • 当 Request 的结束的时候,Cancel Context
  • Context.Value 应该被用于告知性质的事物,而不是控制性质的事物
  • 约束 Context.Value 的键空间
  • Context 以及 Context.Value 应该是不可变的(immutable),并且应该是线程安全
  • Context 应该随 Request 消亡而消亡

Q&A

数据库的访问也用 Context 么?

之前说过长时间、可阻塞的操作都用 Context,数据库操作也是如此。不过对于超时 Cancel 操作来说,一般不会对写操作进行 cancel;但是对于读操作,一般会有 Cancel 操作。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

RocketMQ集群部署配置

发表于 2017-11-22

目标,使用2台机器部署RocketMQ多Master多Slave模式,异步复制集群模式。

第一步,修改/etc/hosts文件

1
2
复制代码192.168.116.115 rocketmq1
192.168.116.116 rocketmq2

集群机器配置完成之后,集群内进行ping一下,确保都通。

编译得到环境

1
2
3
4
5
6
7
复制代码git clone -b develop https://github.com/apache/rocketmq.git
cd rocketmq
mvn -Prelease-all -DskipTests clean install -U
cd distribution/target/apache-rocketmq

本版本是基于4.1.0的,之后apache-rocketmq_4.1.0.tar.gz拷贝集群所有机器进行解压
tar -zxvf apache-rocketmq_4.1.0.tar.gz

配置集群模式【多Master多Slave模式,异步复制】

配置的目录说明:

  • 2m-noslave: 多Master模式
  • 2m-2s-sync: 多Master多Slave模式,同步双写
  • 2m-2s-async:多Master多Slave模式,异步复制

配置项一个主的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
复制代码#所属集群名字
brokerClusterName=rocketmq-cluster
#broker名字,注意此处不同的配置文件填写的不一样
brokerName=broker-a
#0 表示 Master,>0 表示 Slave
brokerId=0
#nameServer地址,分号分割
namesrvAddr=rocketmq1:9876;rocketmq2:9876
#在发送消息时,自动创建服务器不存在的topic,默认创建的队列数
defaultTopicQueueNums=4
#是否允许 Broker 自动创建Topic,建议线下开启,线上关闭
autoCreateTopicEnable=true
#是否允许 Broker 自动创建订阅组,建议线下开启,线上关闭
autoCreateSubscriptionGroup=true
#Broker 对外服务的监听端口
listenPort=10911
#删除文件时间点,默认凌晨 4点
deleteWhen=04
#文件保留时间,默认 72 小时
fileReservedTime=120
#commitLog每个文件的大小默认1G
mapedFileSizeCommitLog=1073741824
#ConsumeQueue每个文件默认存30W条,根据业务情况调整
mapedFileSizeConsumeQueue=300000
#destroyMapedFileIntervalForcibly=120000
#redeleteHangedFileInterval=120000
#检测物理文件磁盘空间
diskMaxUsedSpaceRatio=75
#存储路径
storePathRootDir=/appl/rocketmq/store
#commitLog 存储路径
storePathCommitLog=/appl/rocketmq/store/commitlog
#消费队列存储路径存储路径
storePathConsumeQueue=/appl/rocketmq/store/consumequeue
#消息索引存储路径
storePathIndex=/appl/rocketmq/store/index
#checkpoint 文件存储路径
storeCheckpoint=/appl/rocketmq/store/checkpoint
#abort 文件存储路径
abortFile=/appl/rocketmq/store/abort
#限制的消息大小 默认4M
#maxMessageSize=4194304

#flushCommitLogLeastPages=4
#flushConsumeQueueLeastPages=2
#flushCommitLogThoroughInterval=10000
#flushConsumeQueueThoroughInterval=60000
#Broker 的角色
#- ASYNC_MASTER 异步复制Master
#- SYNC_MASTER 同步双写Master
#- SLAVE
brokerRole=ASYNC_MASTER

#刷盘方式
#- ASYNC_FLUSH 异步刷盘
#- SYNC_FLUSH 同步刷盘
flushDiskType=ASYNC_FLUSH

配置项另外一个备的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
复制代码#所属集群名字
brokerClusterName=rocketmq-cluster
#broker名字,注意此处不同的配置文件填写的不一样
brokerName=broker-b
#0 表示 Master,>0 表示 Slave
brokerId=1
#nameServer地址,分号分割
namesrvAddr=rocketmq1:9876;rocketmq2:9876
#在发送消息时,自动创建服务器不存在的topic,默认创建的队列数
defaultTopicQueueNums=4
#是否允许 Broker 自动创建Topic,建议线下开启,线上关闭
autoCreateTopicEnable=true
#是否允许 Broker 自动创建订阅组,建议线下开启,线上关闭
autoCreateSubscriptionGroup=true
#Broker 对外服务的监听端口
listenPort=11911
#删除文件时间点,默认凌晨 4点
deleteWhen=04
#文件保留时间,默认 72 小时
fileReservedTime=120
#commitLog每个文件的大小默认1G
mapedFileSizeCommitLog=1073741824
#ConsumeQueue每个文件默认存30W条,根据业务情况调整
mapedFileSizeConsumeQueue=300000
#destroyMapedFileIntervalForcibly=120000
#redeleteHangedFileInterval=120000
#检测物理文件磁盘空间
diskMaxUsedSpaceRatio=75
#存储路径
storePathRootDir=/appl/rocketmq-s/store
#commitLog 存储路径
storePathCommitLog=/appl/rocketmq-s/store/commitlog
#消费队列存储路径存储路径
storePathConsumeQueue=/appl/rocketmq-s/store/consumequeue
#消息索引存储路径
storePathIndex=/appl/rocketmq-s/store/index
#checkpoint 文件存储路径
storeCheckpoint=/appl/rocketmq-s/store/checkpoint
#abort 文件存储路径
abortFile=/appl/rocketmq-s/store/abort
#限制的消息大小 默认4M
#maxMessageSize=4194304

#flushCommitLogLeastPages=4
#flushConsumeQueueLeastPages=2
#flushCommitLogThoroughInterval=10000
#flushConsumeQueueThoroughInterval=60000
#Broker 的角色
#- ASYNC_MASTER 异步复制Master
#- SYNC_MASTER 同步双写Master
#- SLAVE
brokerRole=SLAVE

#刷盘方式
#- ASYNC_FLUSH 异步刷盘
#- SYNC_FLUSH 同步刷盘
flushDiskType=ASYNC_FLUSH

备注:由于一台机器部署主备,所以主、备的端口和存储路径会不同,如果多台机器部署就可以保持一样了,主备主要区别在于brokerId、brokerRole配置区别。

注意到logback.*.xml配置文件中${user.home}需要替换自己指定的目录

可以使用sed进行替换:

1
2
3
复制代码sed -i 's#${user.home}#/appl/rocketmq#g' *.xml

# sed -i 's#${user.home}#/appl/rocketmq-s#g' *.xml 由于一台机器部署了2个结点

修改启动脚本中的JVM参数

由于RocketMQ4.1.0官方建议使用64bit JDK 1.8+;比如查看runserver.sh文件jvm参数如下:

1
复制代码-server -Xms4g -Xmx4g -Xmn2g -XX:PermSize=128m -XX:MaxPermSize=320m

需要特别说明下:
元数据空间,专门用来存元数据的,它是jdk8以后用来替代perm的。

JVM堆空间大小根据机器情况进行调整,如果测试机器内存太小需要调整,不然启动不了,比如修改为如下

1
复制代码-Xms1G -Xmx1G -Xmn512m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m

可以使用sed进行替换,把所有PermSize、MaxPermSize替换掉:

1
2
复制代码sed -i 's#PermSize#MetaspaceSize#g' *
sed -i 's#MaxPermSize#MaxMetaspaceSize#g' *

runbroker.sh里面的jvm:

1
复制代码-Xms8g -Xmx8g -Xmn4g

根据自己情况进行修改。

个人建议对于RocketMQ默认的jvm参数除了堆大小其他的先不要调整,后续通过观察分析之后看看是否有必要进行其他参数的调整等,欢迎阅读本人JVM菜鸟进阶高手之路系列文章。

其实我有有疑惑,在broker的jvm参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码JAVA_OPT="${JAVA_OPT} -server -Xms1g -Xmx1g -Xmn512m"
JAVA_OPT="${JAVA_OPT} -XX:+UseG1GC -XX:G1HeapRegionSize=16m
-XX:G1ReservePercent=25 -XX:InitiatingHeapOccupancyPercent=30
-XX:SoftRefLRUPolicyMSPerMB=0 -XX:SurvivorRatio=8 -XX:+DisableExplicitGC"
JAVA_OPT="${JAVA_OPT} -verbose:gc -Xloggc:/dev/shm/mq_gc_%p.log
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCApplicationStoppedTime
-XX:+PrintAdaptiveSizePolicy"
JAVA_OPT="${JAVA_OPT} -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5
-XX:GCLogFileSize=30m"
JAVA_OPT="${JAVA_OPT} -XX:-OmitStackTraceInFastThrow"
JAVA_OPT="${JAVA_OPT} -XX:+AlwaysPreTouch"
JAVA_OPT="${JAVA_OPT} -XX:MaxDirectMemorySize=15g"
JAVA_OPT="${JAVA_OPT} -XX:-UseLargePages -XX:-UseBiasedLocking"
JAVA_OPT="${JAVA_OPT} -Djava.ext.dirs=${BASE_DIR}/lib"
JAVA_OPT="${JAVA_OPT} ${JAVA_OPT_EXT}"
JAVA_OPT="${JAVA_OPT} -cp ${CLASSPATH}"

-XX:+DisableExplicitGC,-XX:MaxDirectMemorySize=15g,但是当达到阀值之后会调用System.gc来做一次full gc,关键System.gc都被禁止了。 希望那位大佬知道,在留言区说明下,谢谢。

启动服务

记住先启动NameServer,再启动Broker,关闭的时候恰好相反,先关闭Broker再关闭NameServer。

启动NameServer:

1
2
3
4
复制代码 nohup sh /appl/apache-rocketmq/bin/mqnamesrv &
tail -f /appl/rocketmq-s/logs/rocketmqlogs/namesrv.log
#tail -f /appl/rocketmq/logs/rocketmqlogs/namesrv.log
INFO main - The Name Server boot success. serializeType=JSON

备注:由于namesrv的端口是写死在代码里面的,所以一台机器启动一个namesrv即可。

启动Broker:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
复制代码rocketmq1机器:
nohup sh /appl/apache-rocketmq/bin/mqbroker -c /appl/apache-rocketmq/conf/2m-2s-async/broker-a.properties >/dev/null 2>&1 &

nohup sh /appl/apache-rocketmq-s/bin/mqbroker -c /appl/apache-rocketmq-s/conf/2m-2s-async/broker-b-s.properties >/dev/null 2>&1 &

rocketmq2机器:
nohup sh /appl/apache-rocketmq/bin/mqbroker -c /appl/apache-rocketmq/conf/2m-2s-async/broker-b.properties >/dev/null 2>&1 &

nohup sh /appl/apache-rocketmq-s/bin/mqbroker -c /appl/apache-rocketmq-s/conf/2m-2s-async/broker-a-s.properties >/dev/null 2>&1 &



tail -f /appl/rocketmq/logs/rocketmqlogs/broker.log
……
INFO main - The broker[broker-a, 192.168.116.116:10911] boot success. serializeType=JSON and name server is rocketmq1:9876;rocketmq2:9876
……

tail -f /appl/rocketmq/logs/rocketmqlogs/namesrv.log
……
new broker registerd, 192.168.116.116:11911 HAServer: 192.168.116.116:11912
……
new broker registerd, 192.168.116.115:11911 HAServer: 192.168.116.115:11912
……

关闭服务:

1
2
复制代码sh bin/mqshutdown broker
sh bin/mqshutdown namesrv

到目前位置,关于RocketMQ的集群环境部署就结束了,未完待续……

如果读完觉得有收获的话,欢迎点赞加关注。


个人公众号,欢迎关注,查阅更多精彩历史!!!

匠心零度公众号

匠心零度公众号

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

宋宝华:火焰图 全局视野的 Linux 性能剖析

发表于 2017-11-22

作者简介:宋宝华,他有10几年的Linux开发经验。他长期在大型企业担任一线工程师和系统架构师,编写大量的Linux代码,并负责在gerrit上review其他同事的代码。Barry Song是Linux的活跃开发者,是某些内核版本的最活跃开发者之一(如lwn.net/Articles/39… 、lwn.net/Articles/42… ),也曾是一ARM SoC系列在Linux mainline的maintainer。

他也是china-pub等据销售评估的2008年度“十大畅销经典”,“十佳原创精品”图书《Linux设备驱动开发详解》的作者和《Essential Linux Device Driver》的译者。同时书写了很多技术文章,是51CTO 2012年度“十大杰出IT博客”得主及51CTO、CSDN的专家博主。他也热衷于开源项目,正在开发LEP(Linux Easy Profiling,www.linuxep.com)项目,并希望获得更多人的参与和帮助。

什么是火焰图

火焰图(Flame Graph)是由Linux性能优化大师Brendan Gregg发明的,和所有其他的trace和profiling方法不同的是,Flame Graph以一个全局的视野来看待时间分布,它从底部往顶部,列出所有可能的调用栈。其他的呈现方法,一般只能列出单一的调用栈或者非层次化的时间分布。

我最快乐的童年时代,每逢冬天,尤其是春节的时候,和一家人围坐在火堆旁边烤火。这已经成为最美好的回忆,其实人生追求的快乐非常简单。火焰图的火焰首先来自于根,然后以火苗的形式往上面窜。可以把从靠近地面的根到顶上的每个火苗,想想成一个调用栈。由于火苗有很多根,这正好也和现实生活中程序的执行逻辑相似。

以典型的分析CPU时间花费到哪个函数的on-cpu火焰图为例来展开。

CPU火焰图中的每一个方框是一个函数,方框的长度,代表了它的执行时间,所以越宽的函数,执行越久。火焰图的楼层每高一层,就是更深一级的函数被调用,最顶层的函数,是叶子函数。

火焰图的生成过程是:

  1. 先trace系统,获取系统的profiling数据
  2. 用脚本来绘制

系统的profiling数据获取,可以选择最流行的perf record,而后把采集的数据进行加工处理,绘制为火焰图。其中第二步的绘制火焰图的脚本程序,通过如下方式获取:

1
复制代码git clone https://github.com/brendangregg/FlameGraph

火焰图案例

废话不多说,直接从最简单的例子开始说起。talk is cheap, show you the cde,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码c()
{
for(int i=0;i<1000;i++);
}
b()
{
for(int i=0;i<1000;i++);
c();
}
a()
{
for(int i=0;i<1000;i++);
b();
}

则这三个函数,在火焰图中呈现的样子为:

a()的2/3的时间花在b()上面,而b()的1/3的时间花在c()上面。很多个这样的a->b->c的火苗堆在一起,就构成了火焰图。

进一步理解火焰图的最好方法仍然是通过一个实际的案例,下面的程序创建2个线程,两个线程的handler都是thread_fun(),之后thread_fun()调用fun_a()、fun_b()、fun_c(),而fun_a()又会调用fun_d():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
复制代码/*
* One example to demo flamegraph
*
* Copyright (c) Barry Song
*
* Licensed under GPLv2
*/

#include <pthread.h>

func_d()
{
int i;
for(i=0;i<50000;i++);
}

func_a()
{
int i;
for(i=0;i<100000;i++);
func_d();
}

func_b()
{
int i;
for(i=0;i<200000;i++);
}

func_c()
{
int i;
for(i=0;i<300000;i++);
}

void* thread_fun(void* param)
{
while(1) {
int i;
for(i=0;i<100000;i++);

func_a();
func_b();
func_c();
}
}

int main(void)
{
pthread_t tid1,tid2;
int ret;

ret=pthread_create(&tid1,NULL,thread_fun,NULL);
if(ret==-1){
...
}

ret=pthread_create(&tid2,NULL,thread_fun,NULL);
...

if(pthread_join(tid1,NULL)!=0){
...
}

if(pthread_join(tid2,NULL)!=0){
...
}

return 0;
}

先看看不用火焰图的缺点在哪里。

如果不用火焰图,我们也可以用类似perf top这样的工具分析出来CPU时间主要花费在哪里了:

1
2
3
复制代码$gcc exam.c -pthread
$./a.out&
$sudo perf top

perf top的显示结果如下:

perf top提示出来了fun_a()、fun_b()、fun_c(), fun_d(),thread_func()这些函数内部的代码是CPU消耗大户,但是它缺乏一个全局的视野,我们无法看出全局的调用栈,也弄不清楚这些函数之间的关系。火焰图则不然,我们用下面的命令可以生成火焰图(以root权限运行):

1
2
3
复制代码perf record -F 99 -a -g -- sleep 60
perf script | ./stackcollapse-perf.pl > out.perf-folded
./flamegraph.pl out.perf-folded > perf-kernel.svg

上述程序捕获系统的行为60秒钟,最后调用flamegraph.pl生成一个火焰图perf-kernel.svg,用看图片的工具就可以打开这个svg。

上述火焰图显示出了a.out中,thread_func()、func_a()、func_b()、fun_c()和func_d()的时间分布。

从上述火焰图可以看出,虽然thread_func()被两个线程调用,但是由于thread_func()之前的调用栈是一样的,所以2个线程的thread_func()调用是合并为同一个方框的。

更深阅读

除了on-cpu的火焰图以外,off-cpu的火焰图,对于分析系统堵在IO、SWAP、取得锁方面的帮助很大,有利于分析系统在运行的时候究竟在等待什么,系统资源之间的彼此伊伴。

比如,下面的火焰图显示,nginx的吞吐能力上不来的很多程度原因在于sem_wait()等待信号量。

上图摘自Yichun Zhang (agentzh)的《Introduction to offCPU Time Flame Graphs》。

关于火焰图的更多细节和更多种火焰图各自的功能,可以访问:
www.brendangregg.com/flamegraphs…

本文来自 Linuxerr 微信公众号

相关阅读

宋宝华: Linux 性能调优的分析与实战

谢宝友:深入理解 RCU 之概念

张亦鸣 : eBPF 简史 (下篇)

此文已由作者授权腾讯云技术社区发布,转载请注明原文出处

原文链接:https://cloud.tencent.com/community/article/934261?utm\_source=juejin

海量技术实践经验,尽在腾讯云社区

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Python生态下的Lisp方言

发表于 2017-11-22

前几天我在这篇Python函数式编程从入门到走火入魔里看到了这样一门Python生态下的Lisp方言—Hy,惊为天人,原来还有这么好玩的东西!虽然我没什么Lisp水平吧,SICP断断续续的读了不知道多长时间了,现在处于一种从入门到放弃的状态中,但我一直还是非常想深入了解一下Lisp和函数式编程的,而且令人兴奋的是它是根植于Python生态的,也就是说可以用Lisp写requests爬虫,写Flask网站,用Pandas分析数据,所有Python生态下极具生产力的库还可以用!还是原来的味道!这是多么cool的事情啊~

https://xkcd.com/224/https://xkcd.com/224/

关于Lisp这门语言的一些情况这里也不多说了,如果你不是特别了解建议阅读这篇Lisp的本质和阮一峰翻译的这篇博文。Hy是众多Lisp方言中的一种,将代码编译成Python的字节码后交给解释器解释运行,Hy之于Python类似于
Clojure(另一种基于JVM的Lisp方言)之于Java,它们都是作为目标语言虚拟机的前端来利用目标语言的生态。

环境配置

安装很简单只需要

1
2
复制代码$ pip install git+https://github.com/hylang/hy.git
# 注意 这里不要直接 pip install hy 这样从pypi直接安装,有坑...我已经踩过了

安装之后可以尝试一下在终端里输入hy然后敲回车就能进入它的REPL,就和Python的REPL一样,你可以通过在里面尝试敲一下代码来学习它的语法和调试代码。 至于编辑器,用vim的话推荐vim-hy这个插件,除了提供语法高亮和缩进外,还能把一些符号排版成类似数学符号的Unicode字符,就像下面这样,看起来挺不(zhuang)错(bi)的。

vim-hyvim-hy
如果你钟爱的编辑器没有hy的插件,那么用cloure的语法高亮和缩进规则也可以。

动手写

在大概看了一下文档之后,写了一段用matplotlib绘图的hy代码,感觉意外的轻松,比原来写别的Lisp的时候感觉好多了,可能是因为它的语义是和Python完全一致的,不好写的地方都可以转换成Python的思路来解决吧,给人的感觉还是在写Python,只不过把函数和方法调用前缀表达式(不熟悉Lisp可能不清楚,就是把表达式的操作符放在最前面,比如 “a+b” 用前缀表达式来写就是 “+ a b” )写在了括号里而已,就像官方文档的Style Guild部分说的“Keep in mind
we’re not Clojure. We’re not Common Lisp. We’re Homoiconic Python, with extra bits that make sense.” 不要把它当成其他的什么语言,只要把它当成长得有点不一样的Python就行了。

1
2
3
4
5
6
7
8
9
10
11
12
复制代码(import [matplotlib [pyplot :as plt]])
(import time)
(.xkcd plt)
(def (, fig, ax) (.subplots plt))
(defn draw-circle [r0 r-max r-step]
(setv r r0)
(if (< r r-max) (do (-> (.Circle plt (, 0.5 0.5) r :color "black" :fill False) (ax.add-artist))
(setv r (+ r r-step))
(draw-circle r r-max r-step))))
(draw-circle 0.2 1 0.02)
(.text plt 0.5 0.5 (.upper "happy hack with hy!") :va "center" :ha "center")
(.show plt)

运行后就能得到这样一副xkcd风格的图像,好像线条有点太密集了,有点辣眼睛… happyhackwhyhappyhackwhy
接下来,我们就来比较详细地了解一下这门语言吧,你不需要懂Lisp(实际上我也不懂( ´・ω) …),只要对Python有所了解,那么你看完接下来的文字后,一定是能够理解它的。

伪装成Lisp的Python

之所以这么说是因为Python中的数据结构、语法特性在Hy中都能找到对应物,首先举几个数据结构的例子(下文示例中的“=>”符号代表Hy解释器的提示符):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
复制代码;字符串
;Hy的字符机制和Python3是一致的,统一使用Unicode,摒弃了Python2的错误
;Hy的字符串有两种写法,第一种在Hy中称为Symbol,语法是在字母前加一个单引号:
=>'hello
u"hello"
=> '你好
u'\u4f60\u597d'
;第二种是双引号括起来的字符串,可以表示多行字符类似于Python中的三引号
=> "[Q]:what is hy?
... [A]:A dialect of Lisp
... that's embedded in Python"
u"[Q]:what is hy?\n[A]:A dialect of Lisp \nthat's embedded in Python"
;列表
;Python中最常用的数据结构在Hy中还是用方括号括起来只不过不用写中间的逗号了
=> [1 2 3 4]
[1L, 2L, 3L, 4L]
=> (def data ["panty" "stocking" "garterbelt"])
=> (get data 1)
u"stocking"
;元组
;用S表达式来表达,操作符是逗号
=> (, "hello" "hy")
(u'hello', u'hy')
;字典
=> {'Sunday '星期天 'Saturday '星期天}
{u'Sunday': u'\u661f\u671f\u5929', u'Saturday': u'\u661f\u671f\u5929'}
=> (print (get _ 'Sunday))
星期天

赋值、控制流、函数定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
复制代码;赋值可用 def 或者 setv 但一般def用于定义全局变量,setv用于定义局部变量
=> (def n 41)
=> (print n)
41
=> (setv n (inc n))
=> (print n)
42
;除了这些,还有Lisp中的let,可以用来显式地创建词法作用域
=> (let [n 'nana
... q 'haqi]
... (print (+ n " " q)))
nana haqi
=> n
42L
=> q
Traceback (most recent call last):
File "<input type="text" />", line 1, in
NameError: name 'q' is not defined
;条件语句
=> (print (if (< n 0.0) "negative" ... (= n 0.0) "zero" ... (> n 0.0) "positive"
... "not a number"))
positive
;或者用cond
=> (print (cond [(< n 0.0) "negative"] ... [(= n 0.0) "zero"] ... [(> n 0.0) "positive"]
... [True "not a number"]))
positive
;for循环
=> (for [x (range 3)] (print x))
0
1
2
;while循环
(while True (print 'hy))
hy
hy
hy
...
;函数定义,没有显式的return,最后一条表达式的求值结果就是返回值
=> (defn fizzbuzz [i]
... (cond [(= 0 (% i 15)) 'FizzBuzz]
... [(= 0 (% i 3)) 'Fizz]
... [(= 0 (% i 5)) 'Buzz]
... [True i]))
;函数调用
=> (fizzbuzz 15)
u'FizzBuzz'

上面的例子是基本的数据结构和控制流,此外Python中的列表推导、with语句、生成器函数等高级语法特性在Hy中也有实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
复制代码;列表推导
=> (list-comp (** x 2) [x (range 10)])
[0L, 1L, 4L, 9L, 16L, 25L, 36L, 49L, 64L, 81L]
;带条件后缀的列表推导
=> (list-comp (** x 2) [x (range 10)] (odd? x))
[1L, 9L, 25L, 49L, 81L]
;字典推导
=> (dict-comp x (* x 2) [x (range 10)] (odd? x))
{1: 2, 3: 6, 9: 18, 5: 10, 7: 14}
;genexpr ;类似于列表推导但产生的是一个generator
=> (def filtered (genexpr x [x (range 10)] (even? x)))
=> (list filtered)
[0, 2, 4, 6, 8]
;with
=> (with [f (open "./test.txt")] (print (.read f)))
first line
second line
;生成器函数(yield)
=> (defn fib []
... (setv (, a b) (, 0 1))
... (while True
... (yield a)
... (setv (, a b) (, b (+ a b)))))
=> (list (take 10 (fib)))
[0L, 1L, 1L, 2L, 3L, 5L, 8L, 13L, 21L, 34L]

当然也少不了面向对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码;这样定义类
=> (defclass FooBar [object]
... "Yet Another Example Class"
...
... (defn --init-- [self x]
... (setv self.x x))
...
... (defn get-x [self]
... "Return our copy of x"
... self.x))
;创建对象
=> (def foobar (FooBar 42))
=> foobar
=> foobar.x
42L
;方法调用
=> (foobar.get-x)
42L
;你也可以用前缀表达式调用类方法,并且更建议这么做
=> (.get-x foobar)
42L

函数式编程

虽然在Hy里所有的东西还是可以用Python的思路来搞,不过既然是Lisp方言,那么在这里一定会比Python有更多的对函数式编程的支持了。根据对函数式编程肤浅的理解,函数式编程的思路是通过函数间的耦合组建数据处理的管道,在写法上尽量少定义中间变量,尽量控制函数副作用。下面举几个Hy中与函数式编程有关的语法特性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码;首先Python中的 map、reduce、filter 以及 lambda 这些函数式编程的基本组件在Hy中还是一样的
;lambda算子(匿名函数) 和可以使用fn或者lambda来表示
=> (fn [x] (+ x 1))
=> (lambda [x] (+ x 1))
<function <lambda> at 0x7f9b61fe1de8>
=> (_ 1)
2L
;Hy中的map、filter和Python3是一致的,返回的是一个可迭代的延迟求值的对象,而非Python2那样直接返回列表
;map
=> (map (fn [x] (* x x)) (range 1 11))
=> (list _) ;这时把它转换成list的时候才会对map进行求值
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
;filter
=> (filter odd? (range 1 11))
=> (list _)
[1, 3, 5, 7, 9]
;reduce
=> (reduce (fn [a b] (* a b)) (range 1 11)) ;从1开始累乘到10
3628800
尾递归优化

函数式编程尽量不修改状态而使用函数的参数来传递状态所以会使用递归的写法,为了避免递归带来的爆栈问题,很多函数是编程语言都会引入了
尾递归优化,Hy中没有那种直接的对函数的尾递归优化,尾递归需要借助外部宏来完成,而且经我测试尾递归优化代码的执行速度比较慢…这还是挺遗憾的。

1
2
3
4
5
6
7
8
9
复制代码(require [hy.contrib.loop [loop]]) ;导入外部宏
(defn factorial [n]
(loop [[i n] [acc 1]]
;通过loop创建递归的开始点,并初始化递归参数
(if (zero? i)
acc
;通过recur进入下一轮递归,如果recur不写在结尾位置就会报错
(recur (dec i) (* acc i)))))
(factorial 1000)
管道

函数式编程中为了尽量不去使用中间变量经常需要对函数进行组合使用,这时就要使用嵌套调用的写法,举一个简单的例子,比如我们要把从0到99的所有数字进行乘方后加1然后再筛选出其中的奇数。用函数式的写法,不使用中间变量,可以这样一行搞定:

1
2
3
复制代码=> (filter odd? (map inc (map (fn [x] (* x x)) (range 100))))
=> (list _)
[1L, 5L, 17L, 37L, 65L, 101L, 145L, 197L, 257L, 325L, 401L, ...]

虽然这样一行就能表达这样的计算过程写起来很爽,但是连续好几层的函数嵌套使得可读性比起命令式编程那样每一步都清楚地写出中间变量的写法差了不少。为了解决这个问题Hy与Clojure一样引入了threading macro,以上面的计算为例,使用threading macro可以这样写:

1
2
3
复制代码=> (->> (range 100) (map (fn [x] (* x x))) (map inc) (filter odd?))
=> (list _)
[1L, 5L, 17L, 37L, 65L, 101L, 145L, 197L, 257L, 325L, 401L, ...]

这里表达式中的操作符“->>”(这里不妨称之为管道操作符)表示该表达式为一个threading,你可以把它看成一个处理数据的流水线或者说是管道(pipeline),其后的第一个表达式会产生产生即将要流经管道的数据,后面的每一个参数都是一个负责加工数据的函数,数据会从左至右流过管道经过加工最后产生输出。可以看到还是一行代码,但这种写法比起函数的嵌套调用要好理解多了。 如果你对Unix shell有所了解,那么你应该很快能理解。没错,这和shell中的管道很像,而且的确可以通过threading
macro来组合命令行调用,就像在shell中做的那样:

1
2
3
复制代码=> (import [sh [cat grep wc]]) ;需要事先安装sh模块: pip install sh
=> (-> (cat "/usr/share/dict/words") (grep "-E" "^hy") (wc "-l"))
210

你也许注意到了Hy中默认的管道操作符有两种:”->”和”->>”,前者会把数据作为后续函数的第一个参数来传递,而后者会将其作为最后一个参数来传递。那如果想让数据从任意的位置流入函数的话应该怎么办呢?这时可以使用外部宏Anaphoric Macros来实现:

1
2
3
4
5
复制代码(require [hy.contrib.anaphoric [ap-pipe]])
=> (ap-pipe 3 (+ it 1) (/ 5 it))
1.25
=> (ap-pipe [4 5 6 7] (list (rest it)) (len it))
3

此外还有面向对象的版本:

1
2
复制代码=> (doto [] (.append 1) (.append 2) .reverse)
[2L, 1L]

来自Lisp的力量:宏

《画手》 埃舍尔《画手》
埃舍尔 宏(Macro)是什么呢?你可能在C语言中使用过宏来对变量进行替换,在Lisp中宏是一种更为强大的机制,它不像C那样通过预处理器来工作而是直接使用Lisp解释器对其进行解释(这意味着宏的地位和其它程序代码是相同的),在Lisp中宏能像函数那样接受参数,得到参数后会对原来的S表达式按照特定方式展开成新的S表达式。在代码被解释的过程中经历了两个阶段,第一个阶段代码本身被当做数据被宏进行展开,第二个阶段才被解释器当做“代码”来进行解释,这体现“代码即数据”这一思想。简单来说宏就是用来生成代码的代码,也就是所谓的元编程,经常有人拿埃舍尔的《画手》中的描绘的场景来进行比喻元编程,用来写代码的代码就仿佛埃舍尔画中那正在画手的手。
接下来开始说一说Hy中的宏。实际上你可能注意到了之前提到的语法特性比如threading还有loop recur和ap-pipe都是通过宏来实现的。除了使用语言内建的宏和用require导入外部宏,我们也可以自己定义宏,举一个例子,我们现在有以下四种计算fibonacci数列的函数写法,需要检验它们的运行效率:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码(require [hy.contrib.loop [loop]])
(import time)
(import sys)
(.setrecursionlimit sys 1000) ;放宽递归深度限制
(defn fib-recur [n] ;递归的写法
(if (in n '(0 1))
(long n)
(+ (fib-recur (- n 1)) (fib-recur (- n 2)))))
(defn fib-iter [n &optional [a 0] [b 1]] ;迭代的写法,但形式上还是递归的
(if (zero? n)
a
(fib-iter (dec n) b (+ a b))))
(defn fib-tco [n] ;使用尾调用优化的写法
(loop [[i n] [a 0] [b 1]]
(if (zero? i)
a
(recur (dec i) b (+ a b)))))
(defn fib-gen [] ;使用生成器的写法
(setv (, a b) (, 0 1))
(while True
(yield a)
(setv (, a b) (, b (+ a b)))))

按照一般的方法检查运行效率,我们需要在目标代码的前后记录当前时刻,然后计算目标代码的运行时间,就像下面这样。

1
2
3
复制代码(def s (.time time))
(-> (fib-iter 500) print)
(-> (- (.time time) s) print)

但我们有四个函数,所以上面的代码就要重复四遍,如果这样做就会显得十分冗长。这时我们就能通过宏来解决这个问题:

1
2
3
4
5
复制代码(defmacro timeit [code &optional [label "code"]]
`(do
(setv s (.time time))
~code
(print (.format "{l} run cost {t}s.\n" :l ~label :t (- (.time time) s)))))

上面这段宏以一个S表达式作为参数,在调用时会被自动展开成其两侧被加上了计时相关代码的形式。然后只需要一行代码就能对代码进行性能分析了:

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码;递归写法的fib计算实在太慢,这里就不考虑它了...
(timeit (-> (fib-iter 500) print) 'fib-iter)
(timeit (-> (fib-tco 500) print) 'fib-tco)
(timeit (-> (nth (fib-gen) 500) print) 'fib-gen)
;运行结果:
;139423224561697880139724382870407283950070256587697307264108962948325571622863290691557658876222521294125
;fib_iter run cost 0.000308990478516s.
;
;139423224561697880139724382870407283950070256587697307264108962948325571622863290691557658876222521294125
;fib_tco run cost 0.000863075256348s.
;
;139423224561697880139724382870407283950070256587697307264108962948325571622863290691557658876222521294125
;fib_gen run cost 0.000124216079712s.

甚至可以通过reader macro让代码变得更加魔性:

1
2
3
4
5
6
7
8
9
复制代码(defreader t [code]
`(do
(setv s (.time time))
~code
(print (.format "code run cost {t}s.\n" :t (- (.time time) s)))))
#t(-> (nth (fib-gen) 500) print)
;结果:
;139423224561697880139724382870407283950070256587697307264108962948325571622863290691557658876222521294125
;code run cost 0.000133037567139s.

迈向实用:与Python交互

Hy根植于Python生态,可以与Python之间无缝衔接,使用Python的库只需要import就行,与Python中存在的那几种import方式是相对应的,比如:

1
2
3
4
复制代码(import requests) ;相当于 import requests
(import [Bio [Seq Align]]) ;from Bio import Seq, Align
(import [Bio [*]]) ;from Bio import *
(import [matplotlib [pyplot :as plt]]) ;from matplotlib import pyplot as plt

在Python中调用Hy代码也是非常容易的,以上面那四个计算fib的函数为例,我把它们保存在名为”fib_bench.hy”的文件里,然后进入Python的REPL:

1
2
3
4
复制代码>>> import hy # 在导入Hy模块前需要先import hy
>>> from fib_bench import *
>>> [fib_iter(i) for i in range(10)]
[0L, 1L, 1L, 2L, 3L, 5L, 8L, 13L, 21L, 34L]

到这里已经可以写可以用于生产的Hy代码了,虽然我目前还没试过,但已经有人这么做了,比如之前提到的hy的vim插件就是用hy本身实现的,还有官方文档上提到的几个例子,比如这个IRC机器人,还有这个django项目,实际上,你到github上搜一下还能找到不少。

结语

个人感觉还是挺喜欢这门语言的,虽然它的社区规模目前还很小,如果程序规模一大还不知道会踩什么坑,但以lisp的语法来组织带有Python语义的代码,以及使用宏来进行更高阶的抽象对于我来说,这所带来的新鲜感实在是太强了。当然以我目前的水平还没有办法成为开发者来直接为它贡献力量,但我是十分乐意用Hy来写一些数据处理和系统管理脚本和为它做一些小小的安利工作的。相信,如果你坚持读到了这里,你也许也会这么想吧。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

如何将golang的并发编程运用到实际开发

发表于 2017-11-22

前言:这几天在写一个工具脚本分析线上的大量的日志文件,本来应该是索然无味的一个工作,但是本着做到极致的原则,激发了我不断思考如何优化。本文将从开发过程中的最开始版本,一点点讲解优化的过程,最终用golang实现了一个类似java的worker线程池,收获满满。

一,无脑开goroutine阶段

1,任务背景
这个工具的作用简单介绍如下:首先是线上的日志量是非常庞大的,然后要去读取日志文件的内容,然后一条条日志项分析,匹配出想要的日志项写入文件,再提取该文件中的数据计算。日志项格式模拟数据如下:

1
2
3
复制代码{
"id":xx,"time":"2017-11-19","key1":"value1","key2":"value2".......
}

2,无脑开gorountine
先给下代码(只放核心代码,省略文件操作和异常错误处理),再来说说这个阶段的思路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
复制代码var wirteChan = make(chan []byte) //用于写入文件
var waitgroup sync.WaitGroup //用于控制同步
func main(){
//省略写入文件的打开操作,毕竟我们主要讲并发这块

//初始化写入的channel
InitWriter(outLogFileWriter)
//接下来去遍历每个日志文件,每读出一个日志项就开一个gorountine去处理
for _,file := range logDir {
if file.IsDir() {
continue
} else {
HandlerFile(arg.dir + "/" + file.Name()) //处理每个文件
}
}
}
/**
* 初始化Writer的channel
*/
func InitWriter(outLogFileWriter *bufio.Writer) {
go func() {
for data := range wirteChan {
nn, err := outLogFileWriter.Write(data)
}
}()
}
//处理每个文件,然后开G去处理每个日志项
func HandlerFile(fileName string) {
file, err := os.Open(fileName)
defer file.Close()
br := bufio.NewReader(file)
for {
data, err := br.ReadBytes('\n')
if err == io.EOF {
break
} else {
go Handler(data) //每次开一个G去处理,处理完写入writeChannel
}
}
}

解析:写博客很不喜欢放大篇幅代码,所以上面给的只是重要的代码,上面代码注释有说到的也不重复说了。好了,我们来想想上面的代码有什么问题?我们现在就假设我们就只有一个非常大的文件,文件中每个记录项都无脑开一个G去执行。那么,运行一下,我们会发现,好慢呀~。问题出在哪里呢?首先我们无法控制G的数量,其次日志文件非常大,这样运行下来,G的数量是非常庞大的,多个G要往一个channel中写数据,那么也会发生严重的阻塞。种种原因,导致了这个方法是不适用的

二,加入带缓冲的任务队列

1,任务队列
在上面我们说到,我们无法控制任务的数量,那么,我在这里就加入了一个任务队列,来对任务进行排队,同时可以控制任务的数量。上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码/**
* Job结构体,包含要处理的数据和处理函数(这个可根据需要修改)
*/
type Job struct {
Data []byte
Proc func([]byte)
}
//Job队列,存储要做的Job,将每个任务打包成Job发送到这里
var JobQueue chan Job = make(chan Job, arg.maxqueue)
//启动处理函数处理
func Handler(Data []byte) {
for range job := <-Queue {
job.Proc(Data)
}
}

解析:在这个时候,抽象出来了任务模型Job,由于函数调用其实就是函数地址加函数参数,所以我们可以将处理函数也放进Job中。然后让处理函数去处理就行了。想到这里,稍微有点佩服自己了,接着兴致勃勃的运行一下。嗯,好像没快多少(其实这个取决了你的处理函数,就是Job中的Proc)。What?冷静下来分析一下,真觉得自己真可爱。我仅仅是对任务进行了包装,然后用了一个带缓冲的任务队列,由于创建的Job远远大于单个M的处理能力,带缓冲只是稍微把问题拖后了一点。

三,Job/Worker模型

其实写到这里,心里对如何优化已经有点B数了。我想起了java中的线程池的概念,我可以建立一个线程池,然后池中包含多个worker(数量可以指定),每个worker去队列中取任务处理,处理完则继续取任务。同时为了提高通用性,参数类型都改为了interface{}。好了,接下来看看代码,这里的代码都很关键,所以就全部放上来了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
复制代码type Job struct {
Data interface{}
Proc func(interface{})
}
//Job队列,存储要做的Job
var JobQueue chan Job = make(chan Job, arg.maxqueue)
//Woker,用来从Job队列中取出Job执行
type Worker struct {
WokerPool chan chan Job //表示属于哪个Worker池,同时接收JobChannel注册
JobChannel chan Job //任务管道,通过这个管道获取任务执行
Quit chan bool //用来停止Worker
}
//新建一个Worker,需要传入Worker池参数
func NewWorker(wokerPool chan chan Job) Worker {
return Worker{
WokerPool: wokerPool,
JobChannel: make(chan Job),
Quit: make(chan bool),
}
}
//Worker的启动:包含:(1) 把该worker的JobChannel注册到WorkerPool中去 (2) 监听JobChannel上有没有新的任务到来 (3) 监听是否受到关闭的请求
func (worker Worker) Start() {
go func() {
for {
worker.WokerPool <- worker.JobChannel //每次做完任务后就重新注册上去通知本worker又处于可用状态了
select {
case job := <-worker.JobChannel:
job.Proc(job.Data)
case quit := <-worker.Quit: //接收到关闭信息,直接退出即可
if quit {
return
}
}
}
}()
}
//Worker的关闭:只要发送一个关闭信号即可
func (worker Worker) Stop() {
go func() {
worker.Quit <- true
}()
}
//管理Worker的调度器,包含最大worker数量和workerpool
type Dispatcher struct {
MaxWorker int
WorkerPool chan chan Job
}

//启动一个调度器
func (dispatcher *Dispatcher) Run() {
//启动maxworker个worker
for i := 0; i < dispatcher.MaxWorker; i++ {
worker := NewWorker(dispatcher.WorkerPool)
worker.Start()
}
//接下来启动调度服务
go dispatcher.dispatch()
}

func (dispatcher *Dispatcher) dispatch() {
for {
select {
case job := <-JobQueue:
go func(job Job) {
jobChannel := <-dispatcher.WorkerPool //获取一个可用的worker
jobChannel <- job //将该job发送给该worker
}(job)
}
}
}

//新建一个调度器
func NewDispatcher(maxWorker int) *Dispatcher {
workerPool := make(chan chan Job, maxWorker)
return &Dispatcher{
WorkerPool: workerPool,
MaxWorker: maxWorker,
}
}

解析:代码中每句都注释得非常清楚了,就不重复了。我们可以通过这样来开启这个模型:dispatcher := NewDispatcher(MaxWorker) dispatcher.Run()。有一点需要强调的是,处理函数这块需要根据自己的业务去写,然后和数据打包成Job再发给JobQueue就行了。接着我运行了我的脚本,几十G的文件经过三轮的处理函数(就是说我需要三轮处理,每轮处理都根据上轮的结果)耗时在三分钟到四分钟之间,而且CPU占用率等也不高。对于耗时高的,可以使用pprof工具分析一下到底慢在了哪里

四,总结

因为之前刚学了golang的并发原理,然后刚好有这个任务,于是自己就开始了从零一点点的摸索和优化,整个工具写完,自己对golang的并发的理解又更加的深入了,而且对锁,文件操作等也熟悉了起来。收获很多东西,所以我鼓励学习一个新东西,不能只懂原理,还要自己多动手一下,这样才牢固。其实这个模型还是存在一些不足之处,后续会继续优化。
期间也参考了一些很不错的博客,在这里也表示感谢。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

有趣的二进制2—高效位运算 一、数独 二、基础 三、应用实例

发表于 2017-11-22

优秀的算法都大量用到位运算,而位运算在工作中很少用到,借助一个示例,我们看一下其的优势以及原理,顺便mark一波常见位运算。

上一篇《有趣的二进制》我们讲到二进制的一些基础知识,但没有讲到位运算,有同学大呼不过瘾,那这一篇主要讲解下位运算的运用,还是从一个例子开始,希望对大家有启发。记得后面例子应用请自行mark,帮助很大。

一、数独

数独是介绍位运算的好例子,运用位运算和不运用效率差别还是挺大的。我们先看数独需求:

1、当前数字所在行数字均含1-9,不重复

2、当前数字所在列数字均含1-9,不重复

3、当前数字所在宫(即3x3的大格)数字均含1-9,不重复(宫,如下图每个粗线内是一个宫)

、

1、常规算法

若是我们采用常规方式的,每填写一个数字,需要检查当前行、列,宫中其他位置是否有重复数字,极端情况下需要循环27(3*9)次来进行检查,我们看下常规算法下check

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码	int check(int sp) {
// 檢查同行、列、九宮格有沒有相同的數字,若有傳回 1
int fg= 0 ;
if(!fg) fg= check1(sp, startH[sp], addH) ; // 檢查同列有沒有相同的數字
if(!fg) fg= check1(sp, startV[sp], addV) ; // 檢查同行有沒有相同的數字
if(!fg) fg= check1(sp, startB[sp], addB) ; // 檢查同九宮格有沒有相同的數字
return(fg) ;
}

int check1(int sp, int start, int *addnum) {
// 檢查指定的行、列、九宮格有沒有相同的數字,若有傳回 1
int fg= 0, i, sp1 ;
//万恶的for循环
for(i=0; i<9; i++) {
sp1= start+ addnum[i] ;
if(sp!=sp1 && sudoku[sp]==sudoku[sp1]) fg++ ;
}
return(fg) ;
}

这个效率是否很吓人,每次填写一个就需要check27次,有木有check一次的算法?当然有了,采用位运算,一次搞定。来我们看下位运算的思路:

2、位运算

我们看上图所示,单个行(或者列、宫)数据,都是有1-9共9个数字,我们统称为九宫数字。若是我们采用二进制,以九宫数字充当二进制数据的位坐标,采用9位的二进制就可以与之一一对应,位上有数据,标识为1,无数据标识为0,如此一个正数就能解决一行九宫数据状态,无需需存一个数组。

比如 看图中深红色部分,当前九宫数据中已经有1和3,那么二进制右起第一位和第三位标识为1,一个数字5就可以存下当前行(或者列、宫)数组状态了,如若数字为511表明,所有的九宫数字都用完了,如图第一行。

check一个数字是否已经被占用了,可以采取位运算来获取二进制的右数第k位来查看是否是1,若是1,表明指定数字已经被占用了。我们看下具体check算法:

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码	// sp 是当前位置索引,indexV 行索引,indexH 列索引,indexB九宫格索引
int check(int sp,int indexV,int indexH,int indexB) {
// 检查同行、列、九宮格沒有用到的数字,若已经用过返回 1
int status = statusV[indexV]|statusH[indexH]|statusB[indexB];
//9个数字都被用了
if (status>=STATUS_MAX_VALUE)
{
return 1;
}
int number=sudoku[sp];
//取右数第k位,若是1表明这个值已经存在了
return status>>(number-1)&1;
}
1
2
3
4
5
6
7
8
9
10
11
复制代码	// 行、列、宫二进制数据指定位置标记为1
int markStatus(int indexV,int indexH,int indexB,int number){
if (number<1)
{
return 0;
}
//把右数第k(从1计数)位变成1
statusV[indexV]|=(1<<(number-1));
statusH[indexH]|=(1<<(number-1));
statusB[indexB]|=(1<<(number-1));
}

我们以以下图例位置举例,如何获得当前位置可以填取的数字

可以看到2个位运算就解决了检查可用数字的操作了,而之前常规算法,需要用27次查找才可以获取到。当然了这个算法还可以优化,比如采用启发式DFS,搜索可用数字,速度更快,感兴趣可点击这里。

常规算法和位运算算法C语言代码,我已经上传码云了,想了解的点击下面链接,自行去查看去。(常规算法google的)

地址: 常规算法数独,位运算版本数独

二、基础

1、位操作符

符号 含义 规则
& 与 两个位都为1时,结果为1
— — —
或
^ 异或 0和1异或0都不变,异或1则取反
~ 取反 0和1全部取反
<< 左移 位全部左移若干位,高位丢弃,低位补0
>> 算术右移 位全部右移若干位,,高位补k个最高有效位的值
>> 逻辑右移 位全部右移若干位,高位补0

注意:

1、位运算只可运用于整数,对于float和double不行。

2、另外逻辑右移符号各种语言不太同,比如java是>>>。

3、位操作符的运算优先级比较低,尽量使用括号来确保运算顺序。比如1&i+1,会先执行i+1再执行&。

三、应用实例

很棒的应用实例,你可以mark一下,方便以后对照使用。

1、混合体

位运算实例

位运算 功能 示例
x >> 1 去掉最后一位 101101->10110
— — —
x << 1 在最后加一个0 101101->1011010
x << 1 1 在最后加一个1
x 1 把最后一位变成1
x & -2 把最后一位变成0 101101->101100
x ^ 1 最后一位取反 101101->101100
x (1 << (k-1)) 把右数第k位变成1
x & ~ (1 << (k-1)) 把右数第k位变成0 101101->101001,k=3
x ^(1 <<(k-1)) 右数第k位取反 101001->101101,k=3
x & 7 取末三位 1101101->101
x & (1 << k-1) 取末k位 1101101->1101,k=5
x >> (k-1) & 1 取右数第k位 1101101->1,k=4
x ((1 << k)-1) 把末k位变成1
x ^ (1 << k-1) 末k位取反 101001->100110,k=4
x & (x+1) 把右边连续的1变成0 100101111->100100000
x (x+1) 把右起第一个0变成1
x (x-1) 把右边连续的0变成1
(x ^ (x+1)) >> 1 取右边连续的1 100101111->1111
x & -x 去掉右起第一个1的左边 100101000->1000
x&0x7F 取末7位 100101000->101000
x& ~0x7F 是否小于127 001111111 & ~0x7F->0
x & 1 判断奇偶 00000111&1->1

2、交换两数

1
2
3
4
5
6
7
8
9
复制代码int swap(int a, int b)  
{
if (a != b)
{
a ^= b;
b ^= a;
a ^= b;
}
}

3、求绝对值

1
2
3
4
5
复制代码int abs(int a)  
{
int i = a >> 31;
return ((a ^ i) - i);
}

4、二分查找32位整数前导0个数

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码int nlz(unsigned x)
{
int n;

if (x == 0) return(32);
n = 1;
if ((x >> 16) == 0) {n = n +16; x = x <<16;}
if ((x >> 24) == 0) {n = n + 8; x = x << 8;}
if ((x >> 28) == 0) {n = n + 4; x = x << 4;}
if ((x >> 30) == 0) {n = n + 2; x = x << 2;}
n = n - (x >> 31);
return n;
}

5、二进制逆序

1
2
3
4
5
6
7
8
9
10
复制代码int reverse_order(int n){

n = ((n & 0xAAAAAAAA) >> 1) | ((n & 0x55555555) << 1);
n = ((n & 0xCCCCCCCC) >> 2) | ((n & 0x33333333) << 2);
n = ((n & 0xF0F0F0F0) >> 4) | ((n & 0x0F0F0F0F) << 4);
n = ((n & 0xFF00FF00) >> 8) | ((n & 0x00FF00FF) << 8);
n = ((n & 0xFFFF0000) >> 16) | ((n & 0x0000FFFF) << 16);

return n;
}

6、 二进制中1的个数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
复制代码 unsigned int BitCount_e(unsigned int value) {
unsigned int count = 0;
// 解释下下面这句话代码,这句话求得两两相加的结果,例如 11 01 00 10
// 11 01 00 10 = 01 01 00 00 + 10 00 00 10,即由奇数位和偶数位相加而成
// 记 value = 11 01 00 10,high_v = 01 01 00 00, low_v = 10 00 00 10
// 则 value = high_v + low_v,high_v 右移一位得 high_v_1,
// 即 high_v_1 = high_v >> 1 = high_v / 2
// 此时 value 可以表示为 value = high_v_1 + high_v_1 + low_v,
// 可见 我们需要 high_v + low_v 的和即等于 value - high_v_1
// 写简单点就是 value = value & 0x55555555 + (value >> 1) & 0x55555555;
value = value - ((value >> 1) & 0x55555555);

// 之后的就好理解了
value = (value & 0x33333333) + ((value >> 2) & 0x33333333);
value = (value & 0x0f0f0f0f) + ((value >> 4) & 0x0f0f0f0f);
value = (value & 0x00ff00ff) + ((value >> 4) & 0x00ff00ff);
value = (value & 0x0000ffff) + ((value >> 8) & 0x0000ffff);
return value;

// 另一种写法,原理一样,原因在最后一种解法中有提到
//value = (value & 0x55555555) + (value >> 1) & 0x55555555;
//value = (value & 0x33333333) + ((value >> 2) & 0x33333333);
//value = (value & 0x0f0f0f0f) + ((value >> 4) & 0x0f0f0f0f);
//value = value + (value >> 8);
//value = value + (value >> 16);
//return (value & 0x0000003f);
}

———————–end————————-

大码候,关注个人成长和技术学习,期待用自己的一点点改变,能够给予你一些启发

扫描关注更多。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

深入理解Golang中的interface(一) 更多阅读

发表于 2017-11-21

原文地址:medium.com/golangspec/…

要点:

  • 接口是系列方法的集合
  • 单个类型可以实现多个方法
  • 同一个接口可以被多种类型实现
  • 接口声明可内嵌其他接口,导入内嵌接口的所有方法(可导出方法+不可导出方法),多层内嵌接口也将全部被导入到接口声明中
  • 禁止接口的循环内嵌
  • 接口内方法名必须唯一:自定义方法和内嵌接口包含方法,名称必须唯一
  • 接口变量可以保存所有实现了此接口的所有类型的值:抽象的理论实现
  • 静态类型VS动态类型:接口类型的变量可以被实现了其接口的类型间相互赋值,动态类型
  • 接口类型变量:动态类型、动态值,只有二者均为零值nil时此接口类型的变量才为nil
  • 空接口:可以承载任何类型的变量,也可以说任何类型都实现(满足)了空接口
  • 接口实现:类型定义了包含某接口声明的所有方法(方法名+签名一致)
  • 接口类型值只能访问接口自身定义的方法,原类型的其他变量无法访问:行为的抽象,联想对比Java中子类赋值给父类时多态特性

关键词:接口(interface),类型(type),方法(method),函数(function),方法签名(signature),可导出方法(exported method),满足(satisfy),实现(implement),

接口(Interface)使得代码更具灵活性、扩展性,是Golang中多态的实现方式。接口允许指定只有一些行为是需要的,而不需要指定一种特定类型。
行为的定义就是一系列方法的集合,如下代码片段一:

1
2
3
4
5
复制代码type I interface {
f1(name string)
f2(name string) (error, float32)
f3() int64
}

并不需要强制的接口实现。我们说类型(type)实现或满足接口(interface),只需要它定义了接口期望的方法名和签名(输入和返回参数),如下代码段二:

1
2
3
4
5
6
7
8
9
10
复制代码type T int64
func (T) f1(name string) {
fmt.Println(name)
}
func (T) f2(name string) (error, float32) {
return nil, 10.2
}
func (T) f3() int64 {
return 10
}

这个例子中类型T满足代码段一中定义的接口I,类型T的值可以作为参数传递给任何将接口I作为接收参数的方法。
如下代码段三:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码type I interface {
M() string
}
type T struct {
name string
}
func (t T) M() string {
return t.name
}
func Hello(i I) {
fmt.Printf("Hi, my name is %s\n", i.M())
}
func main() {
Hello(T{name: "Michał"}) // "Hi, my name is Michał"
}

在函数Hello中,方法调用i.M()通过特定的方式实现:当方法是类型满足的接口的实现时,不同类型的方法可以被调用。
Golang的重要特征:接口是隐式实现的,程序员不需要显示声明类型T实现了接口I。这项工作是由Go编译器自动完成的(永远不要让人去做机器应该做的事情)。这种行为的优雅实现使得如下这种方式成为可能:定义一个接口,这个接口被已经写好的类型自动实现(不需要对之前已完成的类型做修改)。
译者注:这种语言级的特性,可以在新增接口时,既有类型不修改,则自动实现了新的接口。这种多态的方式具有很好的灵活性。


这一特性为接口提供了很大灵活性:单个类型可以实现多个接口,示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码type I1 interface {
M1()
}
type I2 interface {
M2()
}
type T struct{}
func (T) M1() { fmt.Println("T.M1") } // 类型T实现了接口I1
func (T) M2() { fmt.Println("T.M2") } // 类型T实现了接口I2
func f1(i I1) { i.M1() }
func f2(i I2) { i.M2() }
func main() {
t := T{}
f1(t) // "T.M1"
f2(t) // "T.M2"
}

或者同一个接口可以被多种类型实现,示例代码

1
2
3
4
5
6
7
8
9
10
11
12
复制代码type I interface {
M()
}
type T1 struct{}
func (T1) M() { fmt.Println("T1.M") } // 类型T1实现了接口I
type T2 struct{}
func (T2) M() { fmt.Println("T2.M") } // 类型T2实现了接口I
func f(i I) { i.M() }
func main() {
f(T1{}) // "T1.M"
f(T2{}) // "T2.M"
}

除了一个或多个接口要求的方法,类型可以自由实现其他不同的方法。

在Golang中,关于接口的两个概念:

  1. 接口(Interface):实现此接口必须的系列方法,通过关键词interface定义。
  2. 接口类型(Interface type):接口类型变量可以保存实现了任何特定接口的类型值。
    下面分别展开讨论这两个概念。
    定义一个接口

接口定义:方法、内嵌其他接口

接口的声明指定了属于此接口的方法,方法定义则通过其名称和签名(输入和返回参数)完成,如下:

1
2
3
4
5
6
复制代码type I interface { // 定义了一个接口I,它包含四个方法
m1()
m2(int)
m3(int) int
m4() int
}

接口中除了包含方法,也允许内嵌(embedded)其他接口-同一个包内定义或已被导入,此时接口将内嵌接口的所有方法导入到自己的定义中,如下示例:

1
2
3
4
5
6
7
8
9
复制代码import "fmt"
type I interface {
m1()
}
type J interface {
m2()
I
fmt.Stringer
}

接口J包含的方法集为:

  • m1() - 来自内嵌的接口I
  • m2() - 自定义方法
  • String() - 来自fmt.Stringer接口的String()方法(此接口中只有这一个方法)
    接口内方法顺序不关紧要,所以接口内可能出现方法和内嵌接口的交错出现的情况。

接口将导入内嵌接口的所有方法,包含可导出方法(首字母大写的方法)和非导出方法(首字母小写)。

内嵌接口多层内嵌时,导入所有包含接口的方法

如果接口I嵌入了接口J,而接口又内嵌了其他接口K,则接口K的所有方法也将被加入到接口I的声明中,如下:

1
2
3
4
5
6
7
8
9
10
11
复制代码type I interface {
J
i()
}
type J interface {
K
j()
}
type K interface {
k()
}

则接口I包含的方法为:i(), j(), k()

禁止接口的循环内嵌

Circular embedding of interfaces is disallowed and will be detected while compilation (source code):
接口的循环内嵌是被禁止的,正在编译阶段将被检测出错误,如下代码将产生error错误interface type loop involving I:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码type I interface {
J
i()
}
type J interface {
K
j()
}
type K interface {
k()
I
}

接口内的方法名必须唯一

如下代码中自定义方法和内嵌接口包含方法存在命名冲突,则将会抛出编译时错误error:duplicate method i:

1
2
3
4
5
6
7
8
复制代码type I interface {
J
i()
}
type J interface {
j()
i(int)
}

这种接口的组装形式贯穿标准库的各种定义,一个io.ReaderWriter的例子:

1
2
3
4
复制代码type ReadWriter interface {
Reader
Writer
}

至此,我们已经知道了怎么新建一个接口,接下来介绍接口类型的变量(values of interface types)。

接口类型的变量

接口I类型的变量可以保存任何实现了接口I的值,示例如下:

1
2
3
4
5
6
7
8
9
复制代码type I interface {
method1()
}
type T struct{}
func (T) method1() {}
func main() {
var i I = T{} // 变量i是接口类型I的变量, T实现了接口I, 则i可以保存类型为T的变量值
fmt.Println(i)
}

静态类型VS动态类型

变量已有的类型在编译阶段被明确,在声明时指定变量类型,且永不改变,这种情况称为静态类型(static type)或直接简称类型。
接口类型的变量也有一种静态的类型,就是接口自身;他们额外还拥有动态类型(dynamic type),这种类型是可赋值的类型。
示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码type I interface {
M()
}
type T1 struct {}
func (T1) M() {} // 类型T1实现了接口I
type T2 struct {}
func (T2) M() {} // 类型T2实现了接口I
func main() {
var i I = T1{} // 接口类型变量i,可以赋值为T1,也可以被赋值为类型T2
fmt.Printf("%T\n", i) // 输出main.T1
i = T2{}
fmt.Printf("%T\n", i) // 输出main.T2
_ = i
}

变量i的静态类型是接口I,这是不会改变的。
另一方面,动态类型也就是动态变化的,第一次赋值后,i的动态类型为类型T1,然而这并不是固化的,第二次赋值将i的动态类型修改为类型T2。
当接口类型的变量值为空值nil时(接口的零值为nil),则动态类型未被设置。

如何获取接口类型变量的动态类型

  1. reflect包提供获取动态类型的方法,示例代码:
    当变量为零值nil时,reflect包将报错runtime error。
1
2
复制代码fmt.Println(reflect.TypeOf(i).PkgPath(), reflect.TypeOf(i).Name())
fmt.Println(reflect.TypeOf(i).String())
  1. fmt包通过格式化动词%T也可以获取变量的动态类型:
    虽然fmt也是使用reflect来实现的,但当变量i为零值nil时也可以支持。
1
复制代码fmt.Printf("%T\n", i)

接口类型空值nil

看如下的代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码type I interface {
M()
}
type T struct {}
func (T) M() {} // 类型T实现了接口I
func main() {
var t *T // 变量t必然是空值nil
if t == nil {
fmt.Println("t is nil") // 输出这里
} else {
fmt.Println("t is not nil")
}
var i I = t // t是空值,但i呢?
if i == nil {
fmt.Println("i is nil")
} else {
fmt.Println("i is not nil")
}
}

输出结果:

1
2
复制代码t is nil
i is not nil

这个输出结果显然有些吃惊,赋值给变量i的值是nil,但i并不等于nil,接口类型变量包含两个部分:

  • 动态类型(dynamic type)
  • 动态值(dynamic value)
    动态类型在前面章节[动态类型VS静态类型]已介绍。
    动态值是实际变量实际被赋值的值,在上面的例子中var i I = t,变量i的动态值是nil,但i的动态类型是*T。
    通过fmt.Printf("%T\n", i)输出赋值后的变量i的动态类型为*main.T,接口类型变量为nil当且仅当动态类型和动态值均为nil。
    这种情况下,即使接口类型的变量保存了一个空值指针(nil
    pointer),但这个接口变量并不是nil。
    已知的错误是从应该返回接口类型的函数返回未初始化、非接口类型的变量值,示例代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码type I interface {}
type T struct {}
func F() I { // 函数F应该返回接口类型I,在此例中接口类型的返回值=返回类型为*T,值为nil
var t *T
if false { // not reachable but it actually sets value
t = &T{}
}
return t // 这里返回的变量t是空值nil
}
func main() {
fmt.Printf("F() = %v\n", F()) // 返回参数的动态值为nil
fmt.Printf("F() is nil: %v\n", F() == nil) // 返回参数为接口类型,此接口类型的值并不为nil
fmt.Printf("type of F(): %T", F()) // 返回参数类型为类型T
}

打印输出:

1
2
3
复制代码F() = <nil>
F() is nil: false
type of F(): *main.T

just because interface type value returned from function has dynamic value set (*main.T), it isn’t equal to nil.

就是因为函数返回的接口类型值的动态类型为*main.T,所有它不等于空值nil

空的接口

接口的方法集可以完全为空,示例代码:

1
2
3
4
5
6
7
复制代码type I interface {}
type T struct {}
func (T) M() {}
func main() {
var i I = T{}
_ = i
}

空接口自动被任何类型实现,所以任何类型都可以赋值给这种空接口类型的变量。
空接口的动态或静态类型的行为和非空接口一致。
fmt.Println函数中可变参数中空接口广泛使用。
TODO: 可变参数的实现原理?

实现接口

实现方法所有方法的任何类型都自动满足(实现)这个接口。不需要想Java中那样显示声明类型实现了哪个接口。
Go编译器自动检测类型对接口的实现,这是Golang语言级的强大特性。
示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码import (
"fmt"
"regexp"
)
type I interface {
Find(b []byte) []byte // 接口I包含方法Find,而Regexp实现包含此方法实现(方法名+签名),则Regexp实现了接口I
}
func f(i I) {
fmt.Printf("%s\n", i.Find([]byte("abc")))
}
func main() {
var re = regexp.MustCompile(`b`) // 返回类型为*Regexp
f(re)
}

这里我们定义了一个接口I,在没有修改内置的regexp模块的情况下,使得:regexp.Regexp类型实现了接口I。

  • 一个类型可以实现多个接口,一个接口可以被多个类型实现
  • 一个接口实现某接口,赋值给接口类型后,只能访问接口自身定义的方法

接口类型行为的抽象

接口类型值只能访问此接口类型的方法,其隐藏原类型中包含的其他值,比如结构体、数组、scalar等等。
示例代码:

1
2
3
4
5
6
7
8
9
10
11
复制代码type I interface {
M1()
}
type T int64
func (T) M1() {}
func (T) M2() {}
func main() {
var i I = T(10) // 接口类型值i只能访问M1()方法
i.M1()
i.M2() // i.M2 undefined (type I has no field or method M2)
}

更多阅读

  • The Go Programming Language Specification - The Go Programming Language
  • research!rsc: Go Data Structures: Interfaces
  • How to use interfaces in Go

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…936937938…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%