在 YARN 中简化用户日志的管理和使用

Hadoop 的用户日志有很多的用途, 首先最重要的是, 它们能用来调试 MapReduce 应用(application)的问题, 可能是应用本身的问题, 或者在极少数的情况下, 当在集群中执行应用时, 日志可以用来调试硬件或者平台的问题所引起的 task/job 的失败. 其次, 人们可以通过分析历史日志来了解单个任务(task)在 job/workflow 中的历史表现, 甚至可以用 Hadoop 的 MapReduce 来分析 MapReduce 的用户日志(user-logs)来确定性能问题.

在以前, 处理应用(application)生成的用户日志已经成了安装 Hadoop 过程中的一个最大痛点. 在 Hadoop 1.x, 用户日志被 TaskTracker 留在每个节点上, 这样长期来看在各个节点上的用户日志的是难于管理并且也很难被获取和使用到的. YARN 处理日志管理问题的办法是让 NodeManagers(NMs) 提供一个选项, 可以在应用结束后将日志文件安全地移到一个分布式文件系统中, 比如 HDFS.

动机

在 Hadoop 1.x 的各发行版中, MapReduce 是用户唯一可用的编程模型. 每个 MapReduce job 会执行一堆 mapper 和 reducer, 每个 map/reduce task 在本地磁盘上直接生成日志, 比如 syslog/stderr/stdout 或者 profile.out/debug.out 等等. 这些 task 日志可以通过网页读取到, 这种方式也是最方便的方式, 其次就是直接登录到节点服务器上去查看日志文件.

Hadoop 1.x 中日志管理的状态

为了处理用户的 task-logs, TaskTrackers(TTs) 要使用 UserLogManager, 它是由下面几个组件构成:

  • UserLogCleaner: 它在 job 结束后的一段时间后就把这个 job 的日志清理. 这个时间的长度通过 mapred.userlog.retain.hours 配置来调整, 默认值是一天. 它把每个 job 的用户日志文件夹加入线程中等待保留时间后就可以将其删除.
  • 在那之上, 每个 TaskTracker 重启后, 所有在 ${mapred.local.dir}/userlogs 中的日志都会被加到 UserLogCleaner 上.
  • TaskLogsTruncater: 这个组件是在 task 结束后截断超过一定长度的日志文件. 配置 mapreduce.cluster.map.userlog.retain-size 和 mapreduce.cluster.reduce.userlog.retain-size 就是控制日志文件可以保留多长. 这里的做的假设是, 如果一个 task 出错, 它的错误信息通常会出现在日志文件的尾部, 这样当日志文件超出一定大小后我们可以截断丢弃日志的头部内容.
  • mapred.userlog.limit.kb 是早于上面这个的一个解决方案: 当 task 在运行时, 它的 stdout 和 stderr 流数据会导向 unix 的 tail 程序, 并且设置 tail 程序只保留一定大小的日志, 然后写入到 stdout/stderr 文件中.

下面还有一些最终没有被并入生成环境的工作:

  • Log collection: 用户可以启动执行在 client/gateway 机器上的 LogCollector 程序来将每个 job 的日志存入一个紧格式.
  • 杀掉日志文件超过 N GB 的 tasks, 因为一个失控的 task 的日志会填满磁盘导致宕机.

现存日志管理的问题

  • 截断(Truncation): 用户比预期的更频繁抱怨截断日志的问题, 又很少有用户需要所有的日志内容. 不限制日志大小能满足所有人, 100KB 大小能满足一部分人, 但是不是所有.
  • 失控的任务(Run-away tasks): TaskTrackers/DataNodes 还是有可能因为超大的日志文件把磁盘空间给占满, 因为截断只会发生在 task 结束之后.
  • 保留时间不足(Insufficient retention): 用户会抱怨很长的保留时间, 没法满足所有人的需求, 默认的一天保留时间对很多人都是够用的, 但是还是有人抱怨没法做后续的历史分析.
  • 获取(Access): 通过 HTTP 来供日志查看完全依赖于 job 的完成时间和保留时间, 这不是很可靠.
  • 收集状态(Collection status): 同样地, 日志收集工具也没有很好的可靠性, 这种办法需要建立更多的解决方案去检测日志收集器的启动和结束, 以及何时需要从 TTs 管理的日志文件切换到日志收集器.
  • mapred.userlog.limit.kb 增加每个 task 的内存使用, 并且也不支持 YARN 的更一般的 application container.
  • 历史日志分析: 所有通过 HTTP 可以访问的日志仅当这些日志还存在节点服务器上, 如果用户想要做历史日志的分析就需要使用日志收集器以及相关的工具.
  • Load-balancing & SPOF: 所有的日志都写入到一个日志文件夹中, 没有跨磁盘的加载平衡, 如果一个磁盘出了问题, 那么所有 job 的日志都会丢失.

现存的解决方案是解决了最基础的问题, 我们现在有机会去做得更好, 启用平台内的日志聚合.

YARN 中日志聚合的详细描写

NodeManager 就没有像之前 TaskTracker 那样去截断用户日志, 把日志留在每个节点的本地, 它通过提供一个选项可以在应用结束后将日志安全地移动到分布式文件系统中, 比如 HDFS.

  • 对属于同一个应用(Application), 跑在一个 NM 上的所有容器(containers)的日志都会被聚合并写入一个配置好地址的(很可能是压缩过的)日志文件中.
  • 在当前的实现中, 一个应用一结束就会得到如下:
    • 应用级别的日志日志目录和
    • 此节点上该应用的所有的容器的日志文件
  • 用户能通过 YARN 的命令行工具, 网页端或者直接从文件系统(FS)中来查看这些日志, 没必要只局限于网页端一种形式.
  • 通过将日志文件存储在分布式文件系统中, 比起第一代的实现, 现在可以让日志文件存储更长的时间.
  • 我们不再需要去截断日志文件, 只要日志文件的长度是合理的, 我们就会保存整个日志文件.
  • 除此之外, 在容器运行中时, 日志会被写入多个节点的目录中, 这样可以实现有效的负载均衡, 增加容错.
  • AggregatedLogDeletionService 是一个定期删除聚合后的日志的服务. 现在它只运行在 MapReduce JobHistoryServer 中.

用法

网页端访问(Web UI)

通过网页端访问, 需要知道的事情是, 日志的聚合操作用户是感觉不到的.

  • 当 MapReduce 应用还在执行中时, 用户查看日志是通过 ApplicationMaster UI 来访问的, 后者会自动跳转到 NodeManager UI 上.
  • 一旦应用结束, 所有的日志都会归属到 MapReduce 的 JobHistoryServer 中, 此时将由后者来提供日志访问.
  • 对于 非-MapReduce 应用, 由更一般性质的 ApplicationHistoryServer 来提供一样的日志访问服务.

命令行访问

除了可以通过网页端访问, 我们同样可以命令行工具来和日志做交互.

> $HADOOP_YARN_HOME/bin/yarn logs
Retrieve logs for completed YARN applications.
usage: yarn logs -applicationId <application ID> [OPTIONS]

general options are:
-appOwner <Application Owner> AppOwner (assumed to be current user if
not specified)
-containerId <Container ID> ContainerId (must be specified if node
address is specified)
-nodeAddress <Node Address> NodeAddress in the format nodename:port
(must be specified if container id is
specified)

所以, 要打印出一个给定应用的所有日志, 命令如下

> yarn logs -applicationId <application ID>

如果只要一个给定容器的所有日志, 可以如下:

> yarn logs -applicationId <application ID> -containerId <Container ID> -nodeAddress <Node Address>

使用命令行工具的一个显著优势是可以使用常规的 shell 工具, 如 grep, sort 等等, 来过滤出我们想要的信息.

管理

日志相关的一般配置项

  • yarn.nodemanager.log-dirs: 确定当容器还在执行中时, 容器日志在节点上的存储位置, 默认值是 ${yarn.log.dir}/userlogs.
    • 一个应用的本地化日志目录是这样的格式 ${yarn.nodemanager.log-dirs}/application_${appid}
    • 一个独立的容器的日志文件夹会在上面的文件夹下, 文件夹命名格式是这样 container_${containerid}
    • 对于 MapReduce 的应用, 每个容器目录下会包含容器生成的三个文件, stderr, stdin 和 syslog
    • 其他的框架可以自行选择放置多少个日志文件, YARN 对文件名的文件数量都没有限制
  • yarn.log-aggregation-enable: 选择是否启用日志聚合. 如果不启用聚合, NMs 会把日志存储在节点本地(就像第一代所做的那样).

日志聚合启用后的相关配置

  • yarn.nodemanager.remote-app-log-dir: 这是 NMs 将日志聚合后存放在默认的文件系统(一般就是 HDFS 上的)上的地址. 这个地址不应是本地的文件系统, 否则日志服务器会无法提供聚合后日志的能力. 默认值是 /tmp/logs.
  • yarn.nodemanager.remote-app-log-dir-suffix: 日志目录会这样创建 {yarn.nodemanager.remote-app-log-dir}/${user}/{thisParam}, 默认值是 logs.
  • yarn.log-aggregation.retain-seconds: 配置多久后聚合后的日志文件被删除, 配置成 -1 或者一个负值就不会删除聚合日志.
  • yarn.log-aggregation.retain-check-interval-seconds: 确定多长时间去检查一次聚合日志的留存情况以执行日志的删除. 如果设置为 0 或者负值, 那这个值就会用聚合日志的留存时间的十分之一来自动配置, 默认值是 -1.
  • yarn.log.server.url: 一旦一个应用结束, NMs 会将网页访问自动跳转到聚合日志的地址, 现在它指向的是 MapReduce 的 JobHistory.

日志聚合未启用的相关配置

  • yarn.nodemanager.log.retain-seconds: 保存在本地节点的日志的留存时间, 默认值是 10800.
  • yarn.nodemanager.log.deletion-threads-count: 确定 NMs 启动的进程数去删除本地的日志文件.

其他一些配置指导

  • 远端的聚合日志的地址的文件夹权限应该是 1777, ${NMUser} 和 ${NMGroup} 是所有者和所有组.
  • 每个应用层次的日志的权限是 770, 但是文件夹所有人是应用的提交者, 文件夹的所有群组是 ${NMGroup}, 这样安排可以让应用的提交者能访问到聚合后的日志, 并且 ${NMGroup} 可以访问和修改日志.
  • ${NMGroup} 应该是一个有限访问的群组, 这样才不会造成访问泄露.

结论

在这篇文章中, 我们描叙了实现日志聚合的动机以及如何面向用户和管理者. 日志聚合到目前为止证明是一个非常有用的特性.

来源: http://zh.hortonworks.com/blog/simplifying-user-logs-management-and-access-in-yarn/

近来已经习惯使用 Continuum Analytics 公司的Conda产品作为我的主要开发工具箱. Conda提供比隔离环境包 virtualenv 和 python 版本切换包 pyenv 更多, 本文就是带你进行基础环境的搭建和 miniconda(一个轻量级conda的发布包) 的使用.

Conda简介

先介绍一些名词:

  • Continuum Analytics: 开发和发布 conda/miniconda/anaconda 包的公司.

  • Conda: 命令行工具, 类似 pip 或者 virtualenv.

  • Miniconda: conda 的一个轻量级的发布包, 只包含最基础的特性.

  • Anaconda: 一个更丰富的conda包, 包含科学计算python用户的常用包, 这个包在miniconda上层.

Conda其实做的比简单的一个python版本切换和环境隔离的要多, 它就是它自己! 它是一个全新的包管理系统, 包资源库, 它可以管理更多. 在使用conda的同时你还是可以用pip来安装包, 本文就是做一些基础层级的使用的介绍.

安装

官网的安装介绍文档在这里

Conda支持各操作系统的安装, 这里演示安装miniconda, 到官网下载安装包, 在Mac平台下是一个shell文件, 在官网的下载地址里, 注意到有python 2 和python 3 的安装版本, 其实这个不太重要, 因为conda把python也当做是一个可安装的包, 你在安装好conda后依然可以切换python的版本.

下载好后直接执行这个文件

bash Miniconda-latest-MacOSX-x86_64.sh

替换 Virtualenv

Virtualenv 是一个帮你创建隔离环境的包, 在隔离环境里可以使用pip安装包. 同样的功能在conda中通过 conda create 和 conda env 两个命令来实现的.

创建隔离环境

要创建一个运行flask应用的环境:

conda create -n myflaskapp flask

上面这个命令会创建一个名叫myflaskapp的环境, 并且在里面安装好flask包. 要注意的是conda create命令要求在创建新环境的时候必须至少要指定一个要安装的包, 如果没有什么具体的包想指定的可以简单传入python就好了, 如下:

conda create -n myflaskapp python

运行好上面的命令, 你会看到如下的提示, 它教你如何激活和取消这个隔离环境:

#
# To activate this environment, use:
# $ source activate myflaskapp
#
# To deactivate this environment, use:
# $ source deactivate
#

用命令 source activate myflaskapp 激活环境后, 可以使用 conda list 命令来查看已经安装好的包:

(myflaskapp) $ conda list
# packages in environment at /Users/kpurdon/miniconda/envs/myflaskapp:
#
openssl                   1.0.1k                        1
pip                       7.1.0                    py34_0
python                    3.4.3                         0
readline                  6.2                           2
setuptools                18.0.1                   py34_0
sqlite                    3.8.4.1                       1
tk                        8.5.18                        0
xz                        5.0.5                         0
zlib                      1.2.8                         0

注意到上面第三列的 py34_0 所在的行才是安装好的python包, 其他的(python, readline, sqlite, tk, xz, and zlib)都不是python的包. 也可以通过 pip list 来查看安装好的python包.

(myflaskapp) $ pip list
pip (7.1.0)
setuptools (18.0.1)

安装包

Conda提供了完整的pip和pypi的替代, 但是你依然可以使用pip来正常安装包. 我们继续我们的flask例子, 我们依次使用pip和conda来安装.

pip install flask
(myflaskapp) $ conda list
# packages in environment at /Users/kpurdon/miniconda/envs/myflaskapp:
#
flask                     0.10.1                    <pip>
itsdangerous              0.24                      <pip>
jinja2                    2.8                       <pip>
markupsafe                0.23                      <pip>
openssl                   1.0.1k                        1
pip                       7.1.0                    py34_0
python                    3.4.3                         0
readline                  6.2                           2
setuptools                18.0.1                   py34_0
sqlite                    3.8.4.1                       1
tk                        8.5.18                        0
werkzeug                  0.10.4                    <pip>
xz                        5.0.5                         0
zlib                      1.2.8                         0

注意到上面用通过pip安装的包的最后一行都是<pip>, 使用pip list会得到如下:

(myflaskapp) $ pip list
Flask (0.10.1)
itsdangerous (0.24)
Jinja2 (2.8)
MarkupSafe (0.23)
pip (7.1.0)
setuptools (18.0.1)
Werkzeug (0.10.4)

下面使用conda对pip的替代命令来重新安装下, 先移除之前安装的包.

(myflaskapp) $ pip uninstall flask itsdangerous Jinja2 MarkupSafe Werkzeug

执行命令

conda install flask

(myflaskapp) $ conda list
# packages in environment at /Users/kpurdon/miniconda/envs/myflaskapp:
#
flask                     0.10.1                   py34_1
itsdangerous              0.24                     py34_0
jinja2                    2.7.3                    py34_1
markupsafe                0.23                     py34_0
openssl                   1.0.1k                        1
pip                       7.1.0                    py34_0
python                    3.4.3                         0
readline                  6.2                           2
setuptools                18.0.1                   py34_0
sqlite                    3.8.4.1                       1
tk                        8.5.18                        0
werkzeug                  0.10.4                   py34_0
xz                        5.0.5                         0
zlib                      1.2.8                         0

上面可以看到不是通过pip安装的已经移除了<pip>的标志, 说明这些包是从 anaconda.org 网站上下载的. 如果想知道包的具体渠道来源, 可以执行下面的配置命令:

conda config --set show_channel_urls yes

上面的命令会修改你的home文件夹下面的 .condarc 配置文件.

$ cat ~/.condarc
show_channel_urls: yes

现在执行conda list就可以清楚的知道每个包的下载渠道, defaults是Continuum Analytics公司的官网渠道.

(myflaskapp) $ conda list
# packages in environment at /Users/kpurdon/miniconda/envs/myflaskapp:
#
flask                     0.10.1                   py34_1    defaults
itsdangerous              0.24                     py34_0    defaults
jinja2                    2.7.3                    py34_1    defaults
markupsafe                0.23                     py34_0    defaults
openssl                   1.0.1k                        1    http://repo.continuum.io/pkgs/free/osx-64/openssl-1.0.1k-1.tar.bz2
pip                       7.1.0                    py34_0    defaults
python                    3.4.3                         0    defaults
readline                  6.2                           2    <unknown>
setuptools                18.0.1                   py34_0    defaults
sqlite                    3.8.4.1                       1    http://repo.continuum.io/pkgs/free/osx-64/sqlite-3.8.4.1-1.tar.bz2
tk                        8.5.18                        0    http://repo.continuum.io/pkgs/free/osx-64/tk-8.5.18-0.tar.bz2
werkzeug                  0.10.4                   py34_0    defaults
xz                        5.0.5                         0    defaults
zlib                      1.2.8                         0    <unknown>

pip install vs conda install

pip install

  • 优点

    • Python 包的标准选择.

    • 大多数包都先发布到这.

    • 包几乎都是最新的版本.
  • 缺点

    • 安装科学计算包耗时太长, 经常还很多问题.

conda install

  • 优点

    • 和conda集成很好.

    • 安装科学计算包很快很方便.
  • 缺点

    • 包的更新不如pip资源库那么及时.

    • 有些在pip上的包在anaconda.org上没有.

创建 Requirements 文件

文件 requirements.txt 是virtualenv用来存储依赖相应版本的包的列表文件. Conda也有相应的文件, 叫做 environment.yaml, 我们能从一个conda环境中同时创建出两个文件. 如下的conda环境

(myflaskapp) $ conda list
# packages in environment at /Users/kpurdon/miniconda/envs/myflaskapp:
#
flask                     0.10.1                   py34_1    defaults
itsdangerous              0.24                     py34_0    defaults
jinja2                    2.7.3                    py34_1    defaults
markupsafe                0.23                     py34_0    defaults
openssl                   1.0.1k                        1    http://repo.continuum.io/pkgs/free/osx-64/openssl-1.0.1k-1.tar.bz2
pip                       7.1.0                    py34_0    defaults
python                    3.4.3                         0    defaults
readline                  6.2                           2    <unknown>
setuptools                18.0.1                   py34_0    defaults
sqlite                    3.8.4.1                       1    http://repo.continuum.io/pkgs/free/osx-64/sqlite-3.8.4.1-1.tar.bz2
tk                        8.5.18                        0    http://repo.continuum.io/pkgs/free/osx-64/tk-8.5.18-0.tar.bz2
werkzeug                  0.10.4                   py34_0    defaults
xz                        5.0.5                         0    defaults
zlib                      1.2.8                         0    <unknown>

创建 requirements.txt文件:

pip list > requirements.txt
Flask (0.10.1)
itsdangerous (0.24)
Jinja2 (2.7.3)
MarkupSafe (0.23)
pip (7.1.0)
setuptools (18.0.1)
Werkzeug (0.10.4)

创建 environment.yaml文件:

conda env export > environment.yaml

替换pyenv

pyenv是一个让你在一个系统里可以在不同版本的python中安装和切换的包. 通过conda环境也是可以做到这一点的.

创建环境

$ conda create -n py3 python=3*
$ conda create -n py2 python=2*

上面两个命令会创建分别安装python3和python2的环境. 我一般使用其中一个作为的默认环境, 并且把 source activate py3 命令加入我的启动项中, 在某个隔离环境中去执行我的python任务. 对于不同的项目我也会创建不同的隔离环境.

翻译自:
用 Continuum Analytics Conda 作为 virtualenv, pyenv 和其他的替代

简介

reshape 和 reshape2 包是 Hadley Wickham 写的包, 主要在数据的宽格式(wide format)和长格式(long format)之间做转化使用.

 

宽格式和长格式

每个变量作为一个列存储就是宽格式, 如下

#   ozone   wind  temp
# 1 23.62 11.623 65.55
# 2 29.44 10.267 79.10
# 3 59.12  8.942 83.90
# 4 59.96  8.794 83.97

那么如下的就是长格式

#    variable  value
# 1     ozone 23.615
# 2     ozone 29.444
# 3     ozone 59.115
# 4     ozone 59.962
# 5      wind 11.623
# 6      wind 10.267
# 7      wind  8.942
# 8      wind  8.794
# 9      temp 65.548
# 10     temp 79.100
# 11     temp 83.903
# 12     temp 83.968

长格式就是有一个列来存储变量名, 一个列来存储变量值, 当然这样的结构不一定只有两个列, 比如上面的数据就还可有日期的列.

宽格式的数据和长格式数据在实际中都会有自己的用途, 在实际的数据分析中, 长格式的数据用得比较多, ggplot2 需要长格式的数据, plyr 也需要长格式的数据, 并且大多数的模型函数也需要长格式的数据, 但是做数据记录和展现的时候宽格式的数据是很方便的.

reshape2 包

reshape2 包主要围绕两个函数, melt 和 cast 函数.

  • melt 将宽格式数据转换为长格式数据;
  • cast 将长格式数据转换为宽格式数据;

宽转长的 melt 函数

下面使用数据 airquality 来做演示, 为方便起见先将列名转换成小写列名.

library(reshape2)
names(airquality) <- tolower(names(airquality))
head(airquality)
##   ozone solar.r wind temp month day
## 1    41     190  7.4   67     5   1
## 2    36     118  8.0   72     5   2
## 3    12     149 12.6   74     5   3
## 4    18     313 11.5   62     5   4
## 5    NA      NA 14.3   56     5   5
## 6    28      NA 14.9   66     5   6
aql <- melt(airquality) # [a]ir [q]uality [l]ong format
## No id variables; using all as measure variables
head(aql)
##   variable value
## 1    ozone    41
## 2    ozone    36
## 3    ozone    12
## 4    ozone    18
## 5    ozone    NA
## 6    ozone    28
tail(aql)
##     variable value
## 913      day    25
## 914      day    26
## 915      day    27
## 916      day    28
## 917      day    29
## 918      day    30

可以看到上面 melt 将日期也转换为变量名, 所以需要制定变量的 id 列, 如下

aql <- melt(airquality, id.vars = c("month", "day"))
head(aql)
##   month day variable value
## 1     5   1    ozone    41
## 2     5   2    ozone    36
## 3     5   3    ozone    12
## 4     5   4    ozone    18
## 5     5   5    ozone    NA
## 6     5   6    ozone    28

还可以指定长格式数据的变量列名

aql <- melt(airquality, id.vars = c("month", "day"),
  variable.name = "climate_variable", 
  value.name = "climate_value")
head(aql)
##   month day climate_variable climate_value
## 1     5   1            ozone            41
## 2     5   2            ozone            36
## 3     5   3            ozone            12
## 4     5   4            ozone            18
## 5     5   5            ozone            NA
## 6     5   6            ozone            28

长转宽的 cast 函数

在 reshape2 包里的 cast 函数有好几种, dcast 是处理类型为 data.frame 的数据, acast 则是对 vactor, matrix, array 格式的数据, 此处主要以 dcast 函数来做演示.

dcast 函数中使用一个公式 formula 来表示要组合的数据结构, 公式左边是 id 变量, 右边是度量变量所在的列名, 在下面的例子中, 剩下的最后一个列就自动转为变量值的列, 但是在一些其他的情形中需要通过 value.var 参数来明确指出变量值的列名.

aql <- melt(airquality, id.vars = c("month", "day"))
aqw <- dcast(aql, month + day ~ variable)
head(aqw)
##   month day ozone solar.r wind temp
## 1     5   1    41     190  7.4   67
## 2     5   2    36     118  8.0   72
## 3     5   3    12     149 12.6   74
## 4     5   4    18     313 11.5   62
## 5     5   5    NA      NA 14.3   56
## 6     5   6    28      NA 14.9   66

下面这张图很好地解释了 dcast 函数, 其中蓝色的部分的是 id 列, 红色的是变量, 灰色部分是变量值部分.

一个可能会引起“错误”的地方在于一个数据点的位置可能会有多个变量值, 比如如下我们去掉 day 这个变量

dcast(aql, month ~ variable)
## Aggregation function missing: defaulting to length
##   month ozone solar.r wind temp
## 1     5    31      31   31   31
## 2     6    30      30   30   30
## 3     7    31      31   31   31
## 4     8    31      31   31   31
## 5     9    30      30   30   30

执行上面的命令可以看到有一个提示“Aggregation function missing: defaulting to length”, 从上面的数据可以看出, 这样默认得到的是数出了月份的天数. 当做 cast 的时候一个数据位置上会有多个数据时需要制定数据的聚合方式, 如下例子用取均值的聚合.

dcast(aql, month ~ variable, fun.aggregate = mean, 
  na.rm = TRUE)
##   month    ozone  solar.r      wind     temp
## 1     5 23.61538 181.2963 11.622581 65.54839
## 2     6 29.44444 190.1667 10.266667 79.10000
## 3     7 59.11538 216.4839  8.941935 83.90323
## 4     8 59.96154 171.8571  8.793548 83.96774
## 5     9 31.44828 167.4333 10.180000 76.90000

参考: 1. An Introduction to reshape2