Amazon Simple Storage Service (S3) 是一個雲端存儲平台,這是現在蓬勃發展的雲計算的典型應用之一。用戶可以將自己的數據上傳到雲端服務器,便可以隨時隨地地訪問到這些數據,靈活高效。它按需收費,也就是說使用相應容量的存儲空間,就花相應的錢。這裡有具體的資費標準。對於企業用戶來說,使用這項服務實際上可以大大降低成本,這些成本不僅僅包括自己購置服務器硬件、軟件成本,還包括電力、為IT設施維護而僱傭的人力成本等等。

在Amazon S3中有如下幾個概念,通過分別介紹,我們可以大致理解雲存儲的基本原理。

Buckets:一個bucket是一個用於存儲的容器,我們可以不太恰當地理解為就是雲端的文件夾。文件夾要求一個獨特唯一的名字,這和註冊郵箱名差不多,可以加前綴或者後綴來避免重名。bucket使得我們在一個高層級上組織命名空間,並在數據的訪問控制上扮演重要角色。下面舉個例子,假設一個名為photos/puppy.jpg的文件對象存儲在名為johnsmith的bucket里,那麼我們就可以通過這樣一個url訪問到這個對象:http://johnsmith.s3.amazonaws.com/photos/puppy.jpg


Objects:對象,也就是存儲在S3里的基本實體。一個object包括object data和metadata。metadata是一系列的name-value對,用來描述這個object。默認情況下包括文件類型、最後修改時間等等,當然用戶也可以自定義一些metadata。

Keys:即bucket中每一個object的獨一無二的標識符。上面例子中提到的photos/puppy.jpg就是一個key。

Access Control Lists:訪問控制表ACL。在S3中每一個bucket和object都有一個ACL,並且bucket和object的ACL是互相獨立的。當用戶發起一個訪問請求,S3會檢查ACL來核實請求發送者是否有權限訪問這個bucket或object。

Regions:我們可以指定bucket的具體物理存儲區域(Region)。選擇適當的區域可以優化延遲、降低成本。Amazon在世界各地建立了數據中心,目前S3支持下列區域:US Standard,US (Northern California),EU (Ireland),APAC (Singapore)。

雲端為了提高數據可靠性,常用手段是在多個不同的服務器建立同一份數據的冗餘備份(replica)。這樣即使某一個服務器掛了,用戶仍然能夠從別的服務器取得他的數據。使用多份數據副本將帶來數據一致性的問題,如何保證每一份副本的內容是一致的?如何保證多個用戶可以並發讀寫?這在分布式系統設計中是一個經典的問題,我將另寫文章討論。Amazon的US Standard Region為所有的requests提供了最終一致性(eventual consistency),EU 和 Northern California Regions 提供了寫後讀一致性(read-after-write consistency)。

回到應用層面上來。希望開通試用S3雲存儲服務的同學,可以去看看這篇帖子,有詳細步驟和截圖。雖然Amazon給用戶提供了十分友好的Web界面控制台來管理雲端數據和應用,作為開發人員,我們也可以使用boto提供的API建立與Amazon雲計算存儲平台S3交互。boto是一個Amazon雲計算服務的python接口,當然也有其他語言比如C++的接口libAWSJava接口Ruby接口PHP接口,等等。這些API不僅僅用於S3,也可以用於EC2等其他雲計算服務的調用。下面是一個示例程序,擁有連接Amazon S3上傳下載文件等基本功能。

#!/usr/bin/python
#
#  Amazon S3 Interface
#  Author: Zeng, Xi
#  SID:    1010105140
#  Email:  zengxi@cuhk.edu.hk
connected = 0
 
def connect():
    access_key = raw_input('Your access key:').strip()
    secret_key = raw_input('Your secret key:').strip()
    from boto.s3.connection import S3Connection
    global conn
    conn = S3Connection(access_key, secret_key)
    global connected
    connected = 1
 
def creat():
    if connected == 0:
        print 'Not connected!'
    elif connected == 1:
        bucket_name = raw_input('Bucket name:').strip()
        bucket = conn.create_bucket(bucket_name)
 
def put():
    if connected == 0:
        print 'Not connected!'
    elif connected == 1:
        local_file = raw_input('Local filename:').strip()
        bucket = raw_input('Target bucket name:').strip()
        from boto.s3.key import Key
        b = conn.get_bucket(bucket)
        k = Key(b)
        k.key = local_file
        k.set_contents_from_filename(local_file)
 
def ls():
    if connected == 0:
        print 'Not connected!'
    elif connected == 1:
        rs = conn.get_all_buckets()
        for b in rs:
            print b.name
 
def lsfile():
    if connected == 0:
        print 'Not connected!'
    elif connected == 1:
        bucket = raw_input('Bucket name:').strip()
        from boto.s3.key import Key
        b = conn.get_bucket(bucket)
        file_list = b.list()
        for l in file_list:
            print l.name
 
def info():
    if connected == 0:
        print 'Not connected!'
    elif connected == 1:
        bucket = raw_input('Bucket name:').strip()
        filename = raw_input('Filename:').strip()
        from boto.s3.bucketlistresultset import BucketListResultSet
        b = conn.get_bucket(bucket)
        brs = BucketListResultSet(bucket=b)
        for f in brs:
            key = b.lookup(f.name)
            print 'File: ' + f.name
            print 'size: ' + str(key.size)
            print 'last modified: ' + str(key.last_modified)
            print 'etag (md5): ' + str(key.etag)
 
def permission():
    if connected == 0:
        print 'Not connected!'
    elif connected == 1:
        while True:
            bucket = raw_input('Bucket name:').strip()
            permission = raw_input('Permission (private or public-read):').strip()
            if permission not in ['private', 'public-read']:
                print 'Input error!'
            elif permission in ['private', 'public-read']:
                break
        b = conn.get_bucket(bucket)
        b.set_acl(permission)
 
def get():
    if connected == 0:
        print 'Not connected!'
    elif connected == 1:
        bucket = raw_input('Source bucket name:').strip()
        s_file = raw_input('Source filename:').strip()
        d_file = raw_input('Local directory path and filename:').strip()
        from boto.s3.key import Key
        b = conn.get_bucket(bucket)
        key = b.lookup(s_file)
        key.get_contents_to_filename(d_file)
 
def delete():
    if connected == 0:
        print 'Not connected!'
    elif connected == 1:
        bucket = raw_input('Bucket name:').strip()
        conn.delete_bucket(bucket)
 
def delfile():
    if connected == 0:
        print 'Not connected!'
    elif connected == 1:
        bucket = raw_input('Bucket name:').strip()
        filename = raw_input('Filename:').strip()
        b = conn.get_bucket(bucket)
        b.delete_key(filename)
 
def showMenu():
    title = '''
        Amazon S3 Service
 
    connect        Get user credential and connect to Amazon S3
    creat        Creat bucket
    put        Upload file to S3
    ls        List buckets
    lsfile        List files in a bucket
    info        Display information of a file
    permission    Set bucket permissions
    get        Download file from S3
    delete        Delete bucket
    delfile        Delete file
    quit        Quit
 
Enter choice:'''
    while True:
        choice = raw_input(title).strip().lower()
        choices =  ['connect','creat','put','ls','lsfile','info','permission','get','delete','delfile','quit']
        if choice not in choices:
            print('Input Error!')
        else:
            if choice == 'quit':
                break
            elif choice == 'connect':
                connect()
            elif choice == 'creat':
                creat()
            elif choice == 'put':
                put()
            elif choice == 'ls':
                ls()
            elif choice == 'lsfile':
                lsfile()
            elif choice == 'info':
                info()
            elif choice == 'permission':
                permission()
            elif choice == 'get':
                get()
            elif choice == 'delete':
                delete()
            elif choice == 'delfile':
                delfile()
if __name__ == '__main__':
    showMenu()

對於個人用戶來說,文件同步是一個很實用的功能。如果我們的電腦被竊或硬盤損壞,我們仍可以通過同步文件夾從雲端獲取以前的文件。雲存儲也帶來了移動便利,在一些緊急場合,我們甚至可以使用手機來編輯文檔。事實上已經有很多這方面的應用,國外的同步工具Dropbox十分流行,它其實就是以Amazon S3為存儲後台的。國內115網盤之類應用也是層出不窮,金山發布了快盤T盤,迅雷又宣布發布P盤……

下面的python代碼就是使用boto API寫的一個同步文件夾的示例程序。程序通過檢查文件名、大小、MD5來判斷雲端的文件和本地文件夾中的是否相同。如果不同,則下載到本地文件夾。

#!/usr/bin/python
#
#  Synchronize files between local machine and the cloud storage.
#  Author: Zeng, Xi
#  SID:    1010105140
#  Email:  zengxi@cuhk.edu.hk
 
connected = 0
downloaded_files = ""
total_size = 0
 
def connect():
    access_key = raw_input('Your access key:').strip()
    secret_key = raw_input('Your secret key:').strip()
    from boto.s3.connection import S3Connection
    global conn
    conn = S3Connection(access_key, secret_key)
    global connected
    connected = 1
 
def sync():
    if connected == 0:
        print 'Not connected!\n'
        connect()
    if connected == 1:
        bucket = raw_input('Bucket name:').strip()
        local_path = raw_input('Local directory path:').strip()
        from boto.s3.key import Key
        from hashlib import md5
        b = conn.get_bucket(bucket)
        file_list = b.list()
        for l in file_list:
            try:
                F = open(local_path + l.name,"rb")
            except IOError, e:
                get(bucket, l.name, local_path, l.size)
            else:
                s = md5(F.read()).hexdigest()
                if "\""+str(s)+"\"" == str(l.etag):
                    import os
                    local_size = os.path.getsize(local_path + l.name)
                    if int(local_size) == int(l.size):
                        continue
                    else:
                        get(bucket, l.name, local_path, l.size)
                else:
                    get(bucket, l.name, local_path, l.size)
    global downloaded_files
    global total_size
    print "Downloaded files:\n"
    print downloaded_files
    print "Total size:"
    print total_size
 
def get(bucket, filename, local_path, size):
    global downloaded_files
    global total_size
    downloaded_files += filename + "\n"
    total_size += size
    from boto.s3.key import Key
    b = conn.get_bucket(bucket)
    key = b.lookup(filename)
    key.get_contents_to_filename(local_path + filename)
 
if __name__ == '__main__':
    sync()

下載以上程序源代碼:S3接口同步工具
運行前請確認你已經安裝了python和boto

Amazon的雲計算不僅僅是S3數據存儲,還包括EC2虛擬機,SimpleDB數據庫等等很多服務。如果你有興趣,可以查看下面的相關文章。

關於作者:我目前是一名在讀研究生,如果你覺得我的文章對你有用,或我了解的知識對貴公司項目開發有幫助,或許你會有興趣與我聯繫