Loading...

Follow Johng | Golang on Feedspot

Continue with Google
Continue with Facebook
or

Valid
gRPC开发源码包安装 安装官方安装命令: [crayon-5cdbdb8fecb83868929550/] 是安装不起的,会报: [crayon-5cdbdb8fecb88622753219/] 原因是这个代码已经转移到github上面了,但是代码里面的包依赖还是没有修改,还是 google.golang.org 这种地址, 所以不能使用go get的方式安装,正确的安装方式: [crayon-5cdbdb8fecb89814830286/] Protocol Buffers编译文件下载 用以编译proto接口协议文件,直接下载二进制文件:https://github.com/protocolbuffers/protobuf/releases         参考链接: https://www.jianshu.com/p/dba4c7a6d608            
Read Full Article
  • Show original
  • .
  • Share
  • .
  • Favorite
  • .
  • Email
  • .
  • Add Tags 
今日头条当前后端服务超过80%的流量是跑在 Go 构建的服务上。微服务数量超过100个,高峰 QPS 超过700万,日处理请求量超过3000亿,是业内最大规模的 Go 应用。 Go 构建微服务的历程 在2015年之前,头条的主要编程语言是 Python 以及部分 C 。随着业务和流量的快速增长,服务端的压力越来越大,随之而来问题频出。Python 的解释性语言特性以及其落后的多进程服务模型受到了巨大的挑战。此外,当时的服务端架构是一个典型的单体架构,耦合严重,部分独立功能也急需从单体架构中拆出来。 为什么选择 Go 语言? Go 语言相对其它语言具有几点天然的优势: 语法简单,上手快 性能高,编译快,开发效率也不低 原生支持并发,协程模型是非常优秀的服务端模型,同时也适合网络调用 部署方便,编译包小,几乎无依赖 当时 Go 的1.4版本已经发布,我曾在 Go 处于1.1版本的时候,开始使用 Go 语言开发后端组件,并且使用 Go 构建过超大流量的后端服务,因此对 Go 语言本身的稳定性比较有信心。再加上头条后端整体服务化的架构改造,所以决定使用 Go 语言构建今日头条后端的微服务架构。 2015年6月,今日头条开始使用 Go 语言重构后端的 Feed 流服务,期间一边重构,一边迭代现有业务,同时还进行服务拆分,直到2016年6月,Feed 流后端服务几乎全部迁移到 Go。由于期间业务增长较快,夹杂服务拆分,因此没有横向对比重构前后的各项指标。但实际上切换到 Go 语言之后,服务整体的稳定性和性能都大幅提高。 微服务架构 对于复杂的服务间调用,我们抽象出五元组的概念:(From, FromCluster, To, ToCluster,  Method)。每一个五元组唯一定义了一类的RPC调用。以五元组为单元,我们构建了一整套微服务架构。 我们使用 Go 语言研发了内部的微服务框架 …

Read Full Article
  • Show original
  • .
  • Share
  • .
  • Favorite
  • .
  • Email
  • .
  • Add Tags 
可以将不区分大小写的标志设置为正则表达式中的第一项。 你这样做通过添加[crayon-5c3dad31bd222387072641-i/]到正则表达式的开头。 [crayon-5c3dad31bd228981817620/] 对于一个固定的正则表达式它看起来像这样。 [crayon-5c3dad31bd22a607941964/] 有关标志的更多信息,请搜索syntax documentation中的术语“flags”。  
Read Full Article
  • Show original
  • .
  • Share
  • .
  • Favorite
  • .
  • Email
  • .
  • Add Tags 
[crayon-5c3dad31bd4b8693817311/] 执行后,输出结果为: [crayon-5c3dad31bd4bd742797374/]  
Read Full Article
  • Show original
  • .
  • Share
  • .
  • Favorite
  • .
  • Email
  • .
  • Add Tags 
[crayon-5c3dad31bd59b288025529/]  
Read Full Article
  • Show original
  • .
  • Share
  • .
  • Favorite
  • .
  • Email
  • .
  • Add Tags 
ProtoBuf: 是一套完整的 IDL(接口描述语言),出自Google,基于 C 进行的实现,开发人员可以根据 ProtoBuf 的语言规范生成多种编程语言(Golang、Python、Java 等)的接口代码,本篇只讲述 Golang 的基础操作。据说 ProtoBuf 所生成的二进制文件在存储效率上比 XML 高 3~10 倍,并且处理性能高 1~2 个数量级,这也是选择 ProtoBuf 作为序列化方案的一个重要因素之一。 安装 1、安装 protoc :protoc下载地址,可以根据自己的系统下载相应的 protoc,推荐下载编译好的二进制文件包压缩包; 2、配置 protoc 到系统的环境变量中,执行如下命令查看是否安装成功: [crayon-5c3dad31bd67d214784782/] 3、安装 ProtoBuf 相关的 golang 依赖库 [crayon-5c3dad31bd682272532525/] 使用 1、创建 Demo golang工程 2、在 example 包中编写 person.proto [crayon-5c3dad31bd684241237822/] 3、进入 Demo 工程的 example 目录,使用 protoc 编译 person.proto [crayon-5c3dad31bd685622977610/] 就会生成 person.pb.go …

Read Full Article
  • Show original
  • .
  • Share
  • .
  • Favorite
  • .
  • Email
  • .
  • Add Tags 
安装官方安装命令: [crayon-5c3dad31bd7d4219297383/] 是安装不起的,会报: [crayon-5c3dad31bd7d8597590435/] 原因是这个代码已经转移到github上面了,但是代码里面的包依赖还是没有修改,还是 google.golang.org 这种地址, 所以不能使用go get的方式安装,正确的安装方式: [crayon-5c3dad31bd7da728629119/]       参考链接: https://www.jianshu.com/p/dba4c7a6d608            
Read Full Article
  • Show original
  • .
  • Share
  • .
  • Favorite
  • .
  • Email
  • .
  • Add Tags 
Go有很多优点,比如:简单、原生支持并发等,而不错的可移植性也是Go被广大程序员接纳的重要因素之一。但你知道为什么Go语言拥有很好的平台可移植性吗?本着“知其然,亦要知其所以然”的精神,本文我们就来探究一下Go良好可移植性背后的原理。 一、Go的可移植性 说到一门编程语言可移植性,我们一般从下面两个方面考量: 语言自身被移植到不同平台的容易程度; 通过这种语言编译出来的应用程序对平台的适应性。 在Go 1.7及以后版本中,我们可以通过下面命令查看Go支持OS和平台列表: [crayon-5c3dad31bd941798119140/]   从上述列表我们可以看出:从linux/arm64的嵌入式系统到linux/s390x的大型机系统,再到Windows、linux和darwin(mac)这样的主流操作系统、amd64、386这样的主流处理器体系,Go对各种平台和操作系统的支持不可谓不广泛。 Go官方似乎没有给出明确的porting guide,关于将Go语言porting到其他平台上的内容更多是在golang-dev这样的小圈子中讨论的事情。但就Go语言这么短的时间就能很好的支持这么多平台来看,Go的porting还是相对easy的。从个人对Go的了解来看,这一定程度上得益于Go独立实现了runtime。 runtime是支撑程序运行的基础。我们最熟悉的莫过于libc(C运行时),它是目前主流操作系统上应用最普遍的运行时,通常以动态链接库的形式(比如:/lib/x86_64-linux-gnu/libc.so.6)随着系统一并发布,它的功能大致有如下几个: 提供基础库函数调用,比如:strncpy; 封装syscall(注:syscall是操作系统提供的API口,当用户层进行系统调用时,代码会trap(陷入)到内核层面执行),并提供同语言的库函数调用,比如:malloc、fread等; 提供程序启动入口函数,比如:linux下的__libc_start_main。 libc等c runtime lib是很早以前就已经实现的了,甚至有些老旧的libc还是单线程的。一些从事c/c 开发多年的程序员早年估计都有过这样的经历:那就是链接runtime库时甚至需要选择链接支持多线程的库还是只支持单线程的库。除此之外,c runtime的版本也参差不齐。这样的c runtime状况完全不能满足go语言自身的需求;另外Go的目标之一是原生支持并发,并使用goroutine模型,c runtime对此是无能为力的,因为c runtime本身是基于线程模型的。综合以上因素,Go自己实现了runtime,并封装了syscall,为不同平台上的go user level代码提供封装完成的、统一的go标准库;同时Go runtime实现了对goroutine模型的支持。 独立实现的go runtime层将Go user-level code与OS syscall解耦,把Go porting到一个新平台时,将runtime与新平台的syscall对接即可(当然porting工作不仅仅只有这些);同时,runtime层的实现基本摆脱了Go程序对libc的依赖,这样静态编译的Go程序具有很好的平台适应性。比如:一个compiled for linux amd64的Go程序可以很好的运行于不同linux发行版(centos、ubuntu)下。 以下测试试验环境为:darwin amd64 Go 1.8。 二、默认”静态链接”的Go程序 我们先来写两个程序:hello.c和hello.go,它们完成的功能都差不多,在stdout上输出一行文字: [crayon-5c3dad31bd946614905367/] 我们采用“默认”方式分别编译以下两个程序: [crayon-5c3dad31bd948287955015/] 从编译后的两个文件helloc和hellogo的size上我们可以看到hellogo相比于helloc简直就是“巨人”般的存在,其size近helloc的200倍。略微学过一些Go的人都知道,这是因为hellogo中包含了必需的go runtime。我们通过otool工具(linux上可以用ldd)查看一下两个文件的对外部动态库的依赖情况: [crayon-5c3dad31bd94a331641921/] 通过otool输出,我们可以看到hellogo并不依赖任何外部库,我们将hellog这个二进制文件copy到任何一个mac amd64的平台上,均可以运行起来。而helloc则依赖外部的动态库: /usr/lib/libSystem.B.dylib,而libSystem.B.dylib这个动态库还有其他依赖。我们通过nm工具可以查看到helloc具体是哪个函数符号需要由外部动态库提供: [crayon-5c3dad31bd94b910763289/] 可以看到:_printf和dyld_stub_binder两个符号是未定义的(对应的前缀符号是U)。如果对hellog使用nm,你会看到大量符号输出,但没有未定义的符号。 [crayon-5c3dad31bd94d163122630/] Go将所有运行需要的函数代码都放到了hellogo中,这就是所谓的“静态链接”。是不是所有情况下,Go都不会依赖外部动态共享库呢?我们来看看下面这段代码: …

Read Full Article
  • Show original
  • .
  • Share
  • .
  • Favorite
  • .
  • Email
  • .
  • Add Tags 
我们都知道Go语言是原生支持语言级并发的,这个并发的最小逻辑单元就是goroutine。goroutine就类似于Go语言提供的一种“用户态线程”,当然这种“用户态线程”是跑在内核级线程之上的。当我们创建了很多的goroutine,并且它们都是跑在同一个内核线程之上的时候,就需要一个调度器来维护这些goroutine,确保所有的goroutine都使用CPU,并且是尽可能公平的使用CPU资源。 这个调度器的原理以及实现值得我们去深入研究一下。支撑整个调度器的主要有4个重要结构,分别是P、M、G、Sched,前三个定义在runtime.h中,Sched定义在proc.c中。 Sched结构就是调度器,它维护有存储M和G的队列以及调度器的一些状态信息等。 M代表内核级线程,一个M就是一个线程,goroutine就是跑在M之上的;M是一个很大的结构,里面维护小对象内存cache(mcache)、当前执行的goroutine、随机数发生器等等非常多的信息。 P全称是Processor,处理器,它的主要用途就是用来执行goroutine的,所以它也维护了一个goroutine队列,里面存储了所有需要它来执行的goroutine,这个P的角色可能有一点让人迷惑,一开始容易和M冲突,后面重点聊一下它们的关系。 G就是goroutine实现的核心结构了,G维护了goroutine需要的栈、程序计数器以及它所在的M等信息。 理解M、P、G三者的关系对理解整个调度器非常重要,我从网络上找了一个图来说明其三者关系: 地鼠用小车运着一堆待加工的砖。M就可以看作图中的地鼠,P就是小车,G就是小车里装的砖。一图胜千言啊,弄清楚了它们三者的关系,下面我们就开始重点聊地鼠是如何在搬运砖块的。 启动过程 在关心绝大多数程序的内部原理的时候,我们都试图去弄明白其启动初始化过程,弄明白这个过程对后续的深入分析至关重要。在asm_amd64.s文件中的汇编代码_rt0_amd64就是整个启动过程,核心过程如下: [crayon-5c3dad31bdc40197480242/] 启动过程做了调度器初始化runtime.schedinit后,调用runtime.newproc创建出第一个goroutine,这个goroutine将执行的函数是runtime.main,这第一个goroutine也就是所谓的主goroutine。我们写的最简单的Go程序”hello,world”就是完全跑在这个goroutine里,当然任何一个Go程序的入口都是从这个goroutine开始的。最后调用的runtime.mstart就是真正的执行上一步创建的主goroutine。 启动过程中的调度器初始化runtime.schedinit函数主要根据用户设置的GOMAXPROCS值来创建一批小车(P),不管GOMAXPROCS设置为多大,最多也只能创建256个小车(P)。这些小车(p)初始创建好后都是闲置状态,也就是还没开始使用,所以它们都放置在调度器结构(Sched)的[crayon-5c3dad31bdc45288983150-i/]字段维护的链表中存储起来了,以备后续之需。 查看runtime.main函数可以了解到主goroutine开始执行后,做的第一件事情是创建了一个新的内核级线程(地鼠M),不过这个线程是一个特殊线程,它在整个运行期专门负责做特定的事情——系统监控(sysmon)。接下来就是进入Go程序的main函数开始Go程序的执行。 至此,Go程序就被启动起来开始运行了。一个真正干活的Go程序,一定创建有不少的goroutine,所以在Go程序开始运行后,就会向调度器添加goroutine,调度器就要负责维护好这些goroutine的正常执行。 创建goroutine(G) 在Go程序中,时常会有类似代码: [crayon-5c3dad31bdc47677919452/] go关键字就是用来创建一个goroutine的,后面的函数就是这个goroutine需要执行的代码逻辑。go关键字对应到调度器的接口就是[crayon-5c3dad31bdc48946839080-i/]。runtime.newproc干的事情很简单,就负责制造一块砖(G),然后将这块砖(G)放入当前这个地鼠(M)的小车(P)中。 每个新的goroutine都需要有一个自己的栈,G结构的[crayon-5c3dad31bdc4a997484784-i/]字段维护了栈地址以及程序计数器等信息,这是最基本的调度信息,也就是说这个goroutine放弃cpu的时候需要保存这些信息,待下次重新获得cpu的时候,需要将这些信息装载到对应的cpu寄存器中。 假设这个时候已经创建了大量的goroutne,就轮到调度器去维护这些goroutine了。 创建内核线程(M) Go程序中没有语言级的关键字让你去创建一个内核线程,你只能创建goroutine,内核线程只能由runtime根据实际情况去创建。runtime什么时候创建线程?以地鼠运砖图来讲,砖(G)太多了,地鼠(M)又太少了,实在忙不过来,刚好还有空闲的小车(P)没有使用,那就从别处再借些地鼠(M)过来直到把小车(p)用完为止。这里有一个地鼠(M)不够用,从别处借地鼠(M)的过程,这个过程就是创建一个内核线程(M)。创建M的接口函数是: [crayon-5c3dad31bdc4c458886567/] newm函数的核心行为就是调用clone系统调用创建一个内核线程,每个内核线程的开始执行位置都是runtime.mstart函数。参数p就是一辆空闲的小车(p)。 每个创建好的内核线程都从runtime.mstart函数开始执行了,它们将用分配给自己小车去搬砖了。 调度核心 newm接口只是给新创建的M分配了一个空闲的P,也就是相当于告诉借来的地鼠(M)——“接下来的日子,你将使用1号小车搬砖,记住是1号小车;待会自己到停车场拿车。”,地鼠(M)去拿小车(P)这个过程就是[crayon-5c3dad31bdc4d820269652-i/]。runtime.mstart在进入[crayon-5c3dad31bdc4f085251629-i/]之前会给当前M装配上P,runtime.mstart函数中的代码: [crayon-5c3dad31bdc50858108694/] if分支的内容就是为当前M装配上P,[crayon-5c3dad31bdc52947134957-i/]就是newm分配的空闲小车(P),只是到这个时候才真正拿到手罢了。没有P,M是无法执行goroutine的,就像地鼠没有小车无法运砖一样的道理。对应acquirep的动作是releasep,把M装配的P给载掉;活干完了,地鼠需要休息了,就把小车还到停车场,然后睡觉去。 地鼠(M)拿到属于自己的小车(P)后,就进入工场开始干活了,也就是上面的[crayon-5c3dad31bdc53047681691-i/]调用。简化schedule的代码如下: [crayon-5c3dad31bdc55365168204/] schedule函数被我简化了太多,主要是我不喜欢贴大段大段的代码,因此只保留主干代码了。这里涉及到4大步逻辑: [crayon-5c3dad31bdc56372695323-i/], 地鼠(M)试图从自己的小车(P)取出一块砖(G),当然结果可能失败,也就是这个地鼠的小车已经空了,没有砖了。 [crayon-5c3dad31bdc58200976907-i/], 如果地鼠自己的小车中没有砖,那也不能闲着不干活是吧,所以地鼠就会试图跑去工场仓库取一块砖来处理;工场仓库也可能没砖啊,出现这种情况的时候,这个地鼠也没有偷懒停下干活,而是悄悄跑出去,随机盯上一个小伙伴(地鼠),然后从它的车里试图偷一半砖到自己车里。如果多次尝试偷砖都失败了,那说明实在没有砖可搬了,这个时候地鼠就会把小车还回停车场,然后[crayon-5c3dad31bdc59054539346-i/]休息了。如果地鼠睡觉了,下面的过程当然都停止了,地鼠睡觉也就是线程sleep了。 [crayon-5c3dad31bdc5a251438644-i/], 到这个过程的时候,可怜的地鼠发现自己小车里有好多砖啊,自己根本处理不过来;再回头一看停车场居然有闲置的小车,立马跑到宿舍一看,你妹,居然还有小伙伴在睡觉,直接给屁股一脚,“你妹,居然还在睡觉,老子都快累死了,赶紧起来干活,分担点工作。”,小伙伴醒了,拿上自己的小车,乖乖干活去了。有时候,可怜的地鼠跑到宿舍却发现没有在睡觉的小伙伴,于是会很失望,最后只好向工场老板说——”停车场还有闲置的车啊,我快干不动了,赶紧从别的工场借个地鼠来帮忙吧。”,最后工场老板就搞来一个新的地鼠干活了。 [crayon-5c3dad31bdc5c015277936-i/],地鼠拿着砖放入火种欢快的烧练起来。 注: “地鼠偷砖”叫work stealing,一种调度算法。 到这里,貌似整个工场都正常的运转起来了,无懈可击的样子。不对,还有一个疑点没解决啊,假设地鼠的车里有很多砖,它把一块砖放入火炉中后,何时把它取出来,放入第二块砖呢?难道要一直把第一块砖烧练好,才取出来吗?那估计后面的砖真的是等得花儿都要谢了。这里就是要真正解决goroutine的调度,上下文切换问题。 调度点 当我们翻看channel的实现代码可以发现,对channel读写操作的时候会触发调用 runtime.park 函数。goroutine调用park后,这个goroutine就会被设置位waiting状态,放弃CPU。被park的goroutine处于waiting状态,并且这个goroutine不在小车(P)中,如果不对其调用runtime.ready,它是永远不会再被执行的。除了channel操作外,定时器、网络poll等都有可能park goroutine。 除了park可以放弃cpu外,调用 runtime.gosched 函数也可以让当前goroutine放弃cpu,但和park完全不同;gosched是将goroutine设置为runnable状态,然后放入到调度器全局等待队列(也就是上面提到的工场仓库,这下就明白为何工场仓库会有砖块(G)了吧)。 除此之外,就轮到系统调用了,有些系统调用也会触发重新调度。Go语言完全是自己封装的系统调用,所以在封装系统调用的时候,可以做不少手脚,也就是进入系统调用的时候执行entersyscall,退出后又执行exitsyscall函数。 也只有封装了entersyscall的系统调用才有可能触发重新调度,它将改变小车(P)的状态为syscall。还记一开始提到的sysmon线程吗?这个系统监控线程会扫描所有的小车(P),发现一个小车(P)处于了syscall的状态,就知道这个小车(P)遇到了goroutine在做系统调用,于是系统监控线程就会创建一个新的地鼠(M)去把这个处于syscall的小车给抢过来,开始干活,这样这个小车中的所有砖块(G)就可以绕过之前系统调用的等待了。被抢走小车的地鼠等系统调用返回后,发现自己的车没,不能继续干活了,于是只能把执行系统调用的goroutine放回到工场仓库,自己[crayon-5c3dad31bdc5d283182234-i/]去了。 从goroutine的调度点可以看出,调度器还是挺粗暴的,调度粒度有点过大,公平性也没有想想的那么好。总之,这个调度器还是比较简单的。   综上所述,goroutine上下文切换的调度时机可分为以下几个条件: …

Read Full Article
  • Show original
  • .
  • Share
  • .
  • Favorite
  • .
  • Email
  • .
  • Add Tags 
在了解Go的运行时的scheduler之前,需要先了解为什么需要它,因为我们可能会想,OS内核不是已经有一个线程scheduler了嘛? 熟悉POSIX API的人都知道,POSIX的方案在很大程度上是对Unix process进场模型的一个逻辑描述和扩展,两者有很多相似的地方。 Thread有自己的信号掩码,CPU affinity等。但是很多特征对于Go程序来说都是累赘。 尤其是context上下文切换的耗时。另一个原因是Go的垃圾回收需要所有的goroutine停止,使得内存在一个一致的状态。垃圾回收的时间点是不确定的,如果依靠OS自身的scheduler来调度,那么会有大量的线程需要停止工作。 单独的开发一个Go的调度器,可以是其知道在什么时候内存状态是一致的,也就是说,当开始垃圾回收时,运行时只需要为当时正在CPU核上运行的那个线程等待即可,而不是等待所有的线程。 用户空间线程和内核空间线程之间的映射关系有:N:1、1:1和M:N N:1是说,多个(N)用户线程始终在一个内核线程上跑,context上下文切换确实很快,但是无法真正的利用多核。 1:1是说,一个用户线程就只在一个内核线程上跑,这时可以利用多核,但是上下文switch很慢。 M:N是说, 多个goroutine在多个内核线程上跑,这个看似可以集齐上面两者的优势,但是无疑增加了调度的难度。 <img src="https://pic2.zhimg.com/50/2f5c6ef32827fb4fc63c60f4f5314610_hd.jpg" data-rawwidth="391" data-rawheight="103" width="391"> Go的调度器内部有三个重要的结构:M,P,G M:代表真正的内核OS线程,和POSIX里的thread差不多,真正干活的人 G:代表一个goroutine,它有自己的栈,instruction pointer和其他信息(正在等待的channel等等),用于调度。 P:代表调度的上下文,可以把它看做一个局部的调度器,使go代码在一个线程上跑,它是实现从N:1到N:M映射的关键。 <img src="https://pic3.zhimg.com/50/67f09d490f69eec14c1824d939938e14_hd.jpg" data-rawwidth="400" data-rawheight="391" width="400"> 图中看,有2个物理线程M,每一个M都拥有一个context(P),每一个也都有一个正在运行的goroutine。 P的数量可以通过runtime.GOMAXPROCS()来设置,它其实也就代表了真正的并发度,即有多少个goroutine可以同时运行。 图中灰色的那些goroutine并没有运行,而是出于ready的就绪态,正在等待被调度。P维护着这个队列(称之为runqueue), Go语言里,启动一个goroutine很容易:go function 就行,所以每有一个go语句被执行,runqueue队列就在其末尾加入一个 goroutine,在下一个调度点,就从runqueue中取出(如何决定取哪个goroutine?)一个goroutine执行。 为何要维护多个上下文P?因为当一个OS线程被阻塞时,P可以转而投奔另一个OS线程! 图中看到,当一个OS线程M0陷入阻塞时,P转而在OS线程M1上运行。调度器保证有足够的线程来运行所有的context P。 <img src="https://pic1.zhimg.com/50/f1125f3027ebb2bd5183cf8c9ce4b3f2_hd.jpg" data-rawwidth="550" data-rawheight="400" width="550" data-original="https://pic1.zhimg.com/f1125f3027ebb2bd5183cf8c9ce4b3f2_r.jpg"> 图中的M1可能是被创建,或者从线程缓存中取出。当MO返回时,它必须尝试取得一个context P来运行goroutine,一般情况下,它会从其他的OS线程那里steal偷一个context过来, 如果没有偷到的话,它就把goroutine放在一个global runqueue里,然后自己就去睡大觉了(放入线程缓存里)。Contexts们也会周期性的检查global runqueue,否则global runqueue上的goroutine永远无法执行。 <img …

Read Full Article

Read for later

Articles marked as Favorite are saved for later viewing.
close
  • Show original
  • .
  • Share
  • .
  • Favorite
  • .
  • Email
  • .
  • Add Tags 

Separate tags by commas
To access this feature, please upgrade your account.
Start your free month
Free Preview